#
tokens: 5143/50000 8/8 files
lines: off (toggle) GitHub
raw markdown copy
# 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://badge.mcpx.dev?type=server 'MCP Server')
[![smithery badge](https://smithery.ai/badge/@gongrzhe/server-calendar-autoauth-mcp)](https://smithery.ai/server/@gongrzhe/server-calendar-autoauth-mcp)
[![npm version](https://badge.fury.io/js/%40gongrzhe%2Fserver-calendar-autoauth-mcp.svg)](https://www.npmjs.com/package/@gongrzhe/server-calendar-autoauth-mcp)
[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](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);
```