#
tokens: 48611/50000 56/104 files (page 1/6)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 6. Use http://codebase.md/nspady/google-calendar-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .cursorignore
├── .dockerignore
├── .env.example
├── .github
│   └── workflows
│       ├── ci.yml
│       ├── publish.yml
│       └── README.md
├── .gitignore
├── .release-please-manifest.json
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── docker-compose.yml
├── Dockerfile
├── docs
│   ├── advanced-usage.md
│   ├── architecture.md
│   ├── authentication.md
│   ├── deployment.md
│   ├── development.md
│   ├── docker.md
│   ├── README.md
│   └── testing.md
├── examples
│   ├── http-client.js
│   └── http-with-curl.sh
├── future_features
│   └── ARCHITECTURE_REDESIGN.md
├── gcp-oauth.keys.example.json
├── instructions
│   └── file_structure.md
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   ├── account-manager.js
│   ├── build.js
│   ├── dev.js
│   └── test-docker.sh
├── src
│   ├── auth
│   │   ├── client.ts
│   │   ├── paths.d.ts
│   │   ├── paths.js
│   │   ├── server.ts
│   │   ├── tokenManager.ts
│   │   └── utils.ts
│   ├── auth-server.ts
│   ├── config
│   │   └── TransportConfig.ts
│   ├── handlers
│   │   ├── core
│   │   │   ├── BaseToolHandler.ts
│   │   │   ├── BatchRequestHandler.ts
│   │   │   ├── CreateEventHandler.ts
│   │   │   ├── DeleteEventHandler.ts
│   │   │   ├── FreeBusyEventHandler.ts
│   │   │   ├── GetCurrentTimeHandler.ts
│   │   │   ├── GetEventHandler.ts
│   │   │   ├── ListCalendarsHandler.ts
│   │   │   ├── ListColorsHandler.ts
│   │   │   ├── ListEventsHandler.ts
│   │   │   ├── RecurringEventHelpers.ts
│   │   │   ├── SearchEventsHandler.ts
│   │   │   └── UpdateEventHandler.ts
│   │   ├── utils
│   │   │   └── datetime.ts
│   │   └── utils.ts
│   ├── index.ts
│   ├── schemas
│   │   └── types.ts
│   ├── server.ts
│   ├── services
│   │   └── conflict-detection
│   │       ├── config.ts
│   │       ├── ConflictAnalyzer.ts
│   │       ├── ConflictDetectionService.ts
│   │       ├── EventSimilarityChecker.ts
│   │       ├── index.ts
│   │       └── types.ts
│   ├── tests
│   │   ├── integration
│   │   │   ├── claude-mcp-integration.test.ts
│   │   │   ├── direct-integration.test.ts
│   │   │   ├── docker-integration.test.ts
│   │   │   ├── openai-mcp-integration.test.ts
│   │   │   └── test-data-factory.ts
│   │   └── unit
│   │       ├── console-statements.test.ts
│   │       ├── handlers
│   │       │   ├── BatchListEvents.test.ts
│   │       │   ├── BatchRequestHandler.test.ts
│   │       │   ├── CalendarNameResolution.test.ts
│   │       │   ├── create-event-blocking.test.ts
│   │       │   ├── CreateEventHandler.test.ts
│   │       │   ├── datetime-utils.test.ts
│   │       │   ├── duplicate-event-display.test.ts
│   │       │   ├── GetCurrentTimeHandler.test.ts
│   │       │   ├── GetEventHandler.test.ts
│   │       │   ├── list-events-registry.test.ts
│   │       │   ├── ListEventsHandler.test.ts
│   │       │   ├── RecurringEventHelpers.test.ts
│   │       │   ├── UpdateEventHandler.recurring.test.ts
│   │       │   ├── UpdateEventHandler.test.ts
│   │       │   ├── utils-conflict-format.test.ts
│   │       │   └── utils.test.ts
│   │       ├── index.test.ts
│   │       ├── schemas
│   │       │   ├── enhanced-properties.test.ts
│   │       │   ├── no-refs.test.ts
│   │       │   ├── schema-compatibility.test.ts
│   │       │   ├── tool-registration.test.ts
│   │       │   └── validators.test.ts
│   │       ├── services
│   │       │   └── conflict-detection
│   │       │       ├── ConflictAnalyzer.test.ts
│   │       │       └── EventSimilarityChecker.test.ts
│   │       └── utils
│   │           ├── event-id-validator.test.ts
│   │           └── field-mask-builder.test.ts
│   ├── tools
│   │   └── registry.ts
│   ├── transports
│   │   ├── http.ts
│   │   └── stdio.ts
│   ├── types
│   │   └── structured-responses.ts
│   └── utils
│       ├── event-id-validator.ts
│       ├── field-mask-builder.ts
│       └── response-builder.ts
├── tsconfig.json
├── tsconfig.lint.json
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   ".": "2.0.6"
3 | }
4 | 
```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
1 | gcp-oauth.keys.json
2 | .gcp-saved-tokens.json
```

--------------------------------------------------------------------------------
/.cursorignore:
--------------------------------------------------------------------------------

```
1 | .gcp-saved-tokens.json
2 | gcp-oauth.keys.json
3 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | node_modules/
 2 | build/
 3 | *.log
 4 | gcp-oauth.keys.json
 5 | .gcp-saved-tokens.json
 6 | coverage/
 7 | .nyc_output/
 8 | coverage/
 9 | settings.local.json
10 | .env
11 | 
```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
 1 | # Google Calendar MCP Server Configuration
 2 | # Transport mode
 3 | TRANSPORT=stdio  # Recommended for Claude Desktop integration
 4 | 
 5 | # HTTP settings (when TRANSPORT=http)
 6 | PORT=3000
 7 | HOST=0.0.0.0
 8 | 
 9 | # Development
10 | DEBUG=false
11 | NODE_ENV=production
12 | 
13 | # OAuth credentials path (required)
14 | GOOGLE_OAUTH_CREDENTIALS=./gcp-oauth.keys.json
15 | 
16 | ## Optional: Custom token storage location
17 | # GOOGLE_CALENDAR_MCP_TOKEN_PATH=/custom/path/to/tokens
18 | 
19 | ## OPTIONAL: Test Configuration (for development/testing only)
20 | # [email protected]
21 | # SEND_UPDATES=none
22 | # AUTO_CLEANUP=true
23 | 
24 | ## OPTIONAL: Anthropic API config (used only for integration testing)
25 | # ANTHROPIC_MODEL=claude-3-5-haiku-20241022
26 | # CLAUDE_API_KEY={your_api_key}
27 | 
28 | # OPTIONAL: Open AI API config (used only for integration testing)
29 | # OPENAI_API_KEY={your_api_key}
30 | # OPENAI_MODEL=gpt-4.1
```

--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Documentation Index
 2 | 
 3 | Welcome to the Google Calendar MCP Server documentation.
 4 | 
 5 | ## Getting Started
 6 | 
 7 | - [Main README](../README.md) - Quick start guide and overview
 8 | - [Authentication Setup](authentication.md) - Detailed Google Cloud setup instructions
 9 | 
10 | ## User Guides
11 | 
12 | - [Advanced Usage](advanced-usage.md) - Multi-account, batch operations, smart scheduling
13 | - [Troubleshooting](troubleshooting.md) - Common issues and solutions
14 | 
15 | ## Deployment
16 | 
17 | - [Deployment Guide](deployment.md) - HTTP transport, Docker, cloud deployment
18 | 
19 | ## Development
20 | 
21 | - [Development Guide](development.md) - Contributing and development setup
22 | - [Architecture Overview](architecture.md) - Technical architecture details
23 | - [Testing Guide](testing.md) - Running and writing tests
24 | - [Development Scripts](development-scripts.md) - Using the dev command system
25 | 
26 | ## Reference
27 | 
28 | - [API Documentation](https://developers.google.com/calendar/api/v3/reference) - Google Calendar API
29 | - [MCP Specification](https://modelcontextprotocol.io/docs) - Model Context Protocol
30 | 
31 | ## Quick Links
32 | 
33 | ### For Users
34 | 1. Start with the [Main README](../README.md)
35 | 2. Follow [Authentication Setup](authentication.md)
36 | 3. Check [Troubleshooting](troubleshooting.md) if needed
37 | 
38 | ### For Developers
39 | 1. Read [Architecture Overview](architecture.md)
40 | 2. Set up with [Development Guide](development.md)
41 | 3. Run tests with [Testing Guide](testing.md)
42 | 
43 | ### For Deployment
44 | 1. Review [Deployment Guide](deployment.md)
45 | 2. Check security considerations
46 | 3. Set up monitoring
47 | 
48 | ## Need Help?
49 | 
50 | - [GitHub Issues](https://github.com/nspady/google-calendar-mcp/issues)
51 | - [GitHub Discussions](https://github.com/nspady/google-calendar-mcp/discussions)
```

--------------------------------------------------------------------------------
/.github/workflows/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # GitHub Actions Workflows
  2 | 
  3 | This directory contains automated CI/CD workflows for the Google Calendar MCP project.
  4 | 
  5 | ## Workflows
  6 | 
  7 | ### 1. `schema-validation.yml` - Schema Validation and Tests
  8 | **Triggers**: Push/PR to main or develop branches
  9 | 
 10 | Simple workflow focused on schema validation and basic testing:
 11 | - Builds the project
 12 | - Validates MCP schemas for compatibility
 13 | - Runs schema-specific tests
 14 | - Runs unit tests via `npm run dev test`
 15 | 
 16 | ### 2. `ci.yml` - Comprehensive CI Pipeline
 17 | **Triggers**: Push/PR to main, develop, or feature branches
 18 | 
 19 | Full CI pipeline with multiple jobs running in parallel/sequence:
 20 | 
 21 | #### Jobs:
 22 | 1. **code-quality**: 
 23 |    - Checks for console.log statements
 24 |    - Ensures code follows best practices
 25 | 
 26 | 2. **build-and-validate**:
 27 |    - Builds the project
 28 |    - Validates MCP schemas
 29 |    - Uploads build artifacts
 30 | 
 31 | 3. **unit-tests**:
 32 |    - Runs on multiple Node.js versions (18, 20)
 33 |    - Executes all unit tests
 34 |    - Runs schema compatibility tests
 35 | 
 36 | 4. **integration-tests** (optional):
 37 |    - Only runs on main branch or PRs
 38 |    - Executes direct integration tests
 39 |    - Continues on error to not block CI
 40 | 
 41 | 5. **coverage**:
 42 |    - Generates test coverage reports
 43 |    - Uploads coverage artifacts
 44 | 
 45 | ## Running Locally
 46 | 
 47 | To test workflows locally before pushing:
 48 | 
 49 | ```bash
 50 | # Run schema validation
 51 | npm run dev validate-schemas
 52 | 
 53 | # Run dev tests (unit tests only)
 54 | npm run dev test
 55 | 
 56 | # Run all tests
 57 | npm test
 58 | 
 59 | # Run with coverage
 60 | npm run dev coverage
 61 | ```
 62 | 
 63 | ## Environment Variables
 64 | 
 65 | All workflows set `NODE_ENV=test` to:
 66 | - Use test account credentials
 67 | - Skip authentication prompts
 68 | - Enable test-specific behavior
 69 | 
 70 | ## Best Practices
 71 | 
 72 | 1. **Always run `npm run dev test` before pushing** - catches most issues quickly
 73 | 2. **Schema changes** - Run `npm run validate-schemas` to ensure compatibility
 74 | 3. **Console statements** - Use `process.stderr.write()` instead of `console.log()`
 75 | 4. **Integration tests** - These may fail in CI due to API limits; that's OK
 76 | 
 77 | ## Troubleshooting
 78 | 
 79 | ### Schema validation fails
 80 | - Check for `oneOf`, `anyOf`, `allOf` in tool schemas
 81 | - Ensure datetime fields have proper format and timezone info
 82 | - Run `npm run dev validate-schemas` locally
 83 | 
 84 | ### Unit tests fail
 85 | - Run `npm run dev test` locally
 86 | - Check for recent schema changes that might affect tests
 87 | - Ensure all console.log statements are removed
 88 | 
 89 | ### Integration tests fail
 90 | - These are marked as `continue-on-error` in CI
 91 | - Usually due to API rate limits or authentication issues
 92 | - Can be ignored if unit tests pass
 93 | 
 94 | ## Adding New Workflows
 95 | 
 96 | When adding new workflows:
 97 | 1. Test locally first
 98 | 2. Use matrix builds for multiple versions
 99 | 3. Set appropriate environment variables
100 | 4. Consider job dependencies and parallelization
101 | 5. Add documentation here
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Google Calendar MCP Server
  2 | 
  3 | A Model Context Protocol (MCP) server that provides Google Calendar integration for AI assistants like Claude.
  4 | 
  5 | ## Features
  6 | 
  7 | - **Multi-Calendar Support**: List events from multiple calendars simultaneously
  8 | - **Event Management**: Create, update, delete, and search calendar events
  9 | - **Recurring Events**: Advanced modification capabilities for recurring events
 10 | - **Free/Busy Queries**: Check availability across calendars
 11 | - **Smart Scheduling**: Natural language understanding for dates and times
 12 | - **Inteligent Import**: Add calendar events from images, PDFs or web links
 13 | 
 14 | ## Quick Start
 15 | 
 16 | ### Prerequisites
 17 | 
 18 | 1. A Google Cloud project with the Calendar API enabled
 19 | 2. OAuth 2.0 credentials (Desktop app type)
 20 | 
 21 | ### Google Cloud Setup
 22 | 
 23 | 1. Go to the [Google Cloud Console](https://console.cloud.google.com)
 24 | 2. Create a new project or select an existing one.
 25 | 3. Enable the [Google Calendar API](https://console.cloud.google.com/apis/library/calendar-json.googleapis.com) for your project. Ensure that the right project is selected from the top bar before enabling the API.
 26 | 4. Create OAuth 2.0 credentials:
 27 |    - Go to Credentials
 28 |    - Click "Create Credentials" > "OAuth client ID"
 29 |    - Choose "User data" for the type of data that the app will be accessing
 30 |    - Add your app name and contact information
 31 |    - Add the following scopes (optional):
 32 |      - `https://www.googleapis.com/auth/calendar.events` and `https://www.googleapis.com/auth/calendar`
 33 |    - Select "Desktop app" as the application type (Important!)
 34 |    - Save the auth key, you'll need to add its path to the JSON in the next step
 35 |    - Add your email address as a test user under the [Audience screen](https://console.cloud.google.com/auth/audience)
 36 |       - Note: it might take a few minutes for the test user to be added. The OAuth consent will not allow you to proceed until the test user has propagated.
 37 |       - Note about test mode: While an app is in test mode the auth tokens will expire after 1 week and need to be refreshed (see Re-authentication section below).
 38 | 
 39 | ### Installation
 40 | 
 41 | **Option 1: Use with npx (Recommended)**
 42 | 
 43 | Add to your Claude Desktop configuration:
 44 | 
 45 | **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
 46 | **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
 47 | ```json
 48 | {
 49 |   "mcpServers": {
 50 |     "google-calendar": {
 51 |       "command": "npx",
 52 |       "args": ["@cocal/google-calendar-mcp"],
 53 |       "env": {
 54 |         "GOOGLE_OAUTH_CREDENTIALS": "/path/to/your/gcp-oauth.keys.json"
 55 |       }
 56 |     }
 57 |   }
 58 | }
 59 | ```
 60 | 
 61 | **⚠️ Important Note for npx Users**: When using npx, you **must** specify the credentials file path using the `GOOGLE_OAUTH_CREDENTIALS` environment variable.
 62 | 
 63 | **Option 2: Local Installation**
 64 | 
 65 | ```bash
 66 | git clone https://github.com/nspady/google-calendar-mcp.git
 67 | cd google-calendar-mcp
 68 | npm install
 69 | npm run build
 70 | ```
 71 | 
 72 | Then add to Claude Desktop config using the local path or by specifying the path with the `GOOGLE_OAUTH_CREDENTIALS` environment variable.
 73 | 
 74 | **Option 3: Docker Installation**
 75 | 
 76 | ```bash
 77 | git clone https://github.com/nspady/google-calendar-mcp.git
 78 | cd google-calendar-mcp
 79 | cp /path/to/your/gcp-oauth.keys.json .
 80 | docker compose up
 81 | ```
 82 | 
 83 | See the [Docker deployment guide](docs/docker.md) for detailed configuration options including HTTP transport mode.
 84 | 
 85 | ### First Run
 86 | 
 87 | 1. Start Claude Desktop
 88 | 2. The server will prompt for authentication on first use
 89 | 3. Complete the OAuth flow in your browser
 90 | 4. You're ready to use calendar features!
 91 | 
 92 | ### Re-authentication
 93 | 
 94 | If you're in test mode (default), tokens expire after 7 days. If you are using a client like Claude Desktop it should open up a browser window to automatically re-auth. However, if you see authentication errors you can also resolve by following these steps:
 95 | 
 96 | **For npx users:**
 97 | ```bash
 98 | export GOOGLE_OAUTH_CREDENTIALS="/path/to/your/gcp-oauth.keys.json"
 99 | npx @cocal/google-calendar-mcp auth
100 | ```
101 | 
102 | **For local installation:**
103 | ```bash
104 | npm run auth
105 | ```
106 | 
107 | **To avoid weekly re-authentication**, publish your app to production mode (without verification):
108 | 1. Go to Google Cloud Console → "APIs & Services" → "OAuth consent screen"
109 | 2. Click "PUBLISH APP" and confirm
110 | 3. Your tokens will no longer expire after 7 days but Google will show a more threatning warning when connecting to the app about it being unverified. 
111 | 
112 | See [Authentication Guide](docs/authentication.md#moving-to-production-mode-recommended) for details.
113 | 
114 | ## Example Usage
115 | 
116 | Along with the normal capabilities you would expect for a calendar integration you can also do really dynamic, multi-step processes like:
117 | 
118 | 1. **Cross-calendar availability**:
119 |    ```
120 |    Please provide availability looking at both my personal and work calendar for this upcoming week.
121 |    I am looking for a good time to meet with someone in London for 1 hr.
122 |    ```
123 | 
124 | 2. Add events from screenshots, images and other data sources:
125 |    ```
126 |    Add this event to my calendar based on the attached screenshot.
127 |    ```
128 |    Supported image formats: PNG, JPEG, GIF
129 |    Images can contain event details like date, time, location, and description
130 | 
131 | 3. Calendar analysis:
132 |    ```
133 |    What events do I have coming up this week that aren't part of my usual routine?
134 |    ```
135 | 4. Check attendance:
136 |    ```
137 |    Which events tomorrow have attendees who have not accepted the invitation?
138 |    ```
139 | 5. Auto coordinate events:
140 |    ```
141 |    Here's some available that was provided to me by someone. {available times}
142 |    Take a look at the times provided and let me know which ones are open on my calendar.
143 |    ```
144 | 
145 | ## Available Tools
146 | 
147 | | Tool | Description |
148 | |------|-------------|
149 | | `list-calendars` | List all available calendars |
150 | | `list-events` | List events with date filtering |
151 | | `search-events` | Search events by text query |
152 | | `create-event` | Create new calendar events |
153 | | `update-event` | Update existing events |
154 | | `delete-event` | Delete events |
155 | | `get-freebusy` | Check availability across calendars, including external calendars |
156 | | `list-colors` | List available event colors |
157 | 
158 | ## Documentation
159 | 
160 | - [Authentication Setup](docs/authentication.md) - Detailed Google Cloud setup
161 | - [Advanced Usage](docs/advanced-usage.md) - Multi-account, batch operations
162 | - [Deployment Guide](docs/deployment.md) - HTTP transport, remote access
163 | - [Docker Guide](docs/docker.md) - Docker deployment with stdio and HTTP modes
164 | - [OAuth Verification](docs/oauth-verification.md) - Moving from test to production mode
165 | - [Architecture](docs/architecture.md) - Technical architecture overview
166 | - [Development](docs/development.md) - Contributing and testing
167 | - [Testing](docs/testing.md) - Unit and integration testing guide
168 | 
169 | ## Configuration
170 | 
171 | **Environment Variables:**
172 | - `GOOGLE_OAUTH_CREDENTIALS` - Path to OAuth credentials file
173 | - `GOOGLE_CALENDAR_MCP_TOKEN_PATH` - Custom token storage location (optional)
174 | 
175 | **Claude Desktop Config Location:**
176 | - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
177 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
178 | 
179 | 
180 | ## Security
181 | 
182 | - OAuth tokens are stored securely in your system's config directory
183 | - Credentials never leave your local machine
184 | - All calendar operations require explicit user consent
185 | 
186 | ### Troubleshooting
187 | 
188 | 1. **OAuth Credentials File Not Found:**
189 |    - For npx users: You **must** specify the credentials file path using `GOOGLE_OAUTH_CREDENTIALS`
190 |    - Verify file paths are absolute and accessible
191 | 
192 | 2. **Authentication Errors:**
193 |    - Ensure your credentials file contains credentials for a **Desktop App** type
194 |    - Verify your user email is added as a **Test User** in the Google Cloud OAuth Consent screen
195 |    - Try deleting saved tokens and re-authenticating
196 |    - Check that no other process is blocking ports 3000-3004
197 | 
198 | 3. **Build Errors:**
199 |    - Run `npm install && npm run build` again
200 |    - Check Node.js version (use LTS)
201 |    - Delete the `build/` directory and run `npm run build`
202 | 4. **"Something went wrong" screen during browser authentication**
203 |    - Perform manual authentication per the below steps
204 |    - Use a Chromium-based browser to open the authentication URL. Test app authentication may not be supported on some non-Chromium browsers.
205 | 
206 | 5. **"User Rate Limit Exceeded" errors**
207 |    - This typically occurs when your OAuth credentials are missing project information
208 |    - Ensure your `gcp-oauth.keys.json` file includes `project_id`
209 |    - Re-download credentials from Google Cloud Console if needed
210 |    - The file should have format: `{"installed": {"project_id": "your-project-id", ...}}`
211 | 
212 | ### Manual Authentication
213 | For re-authentication or troubleshooting:
214 | ```bash
215 | # For npx installations
216 | export GOOGLE_OAUTH_CREDENTIALS="/path/to/your/credentials.json"
217 | npx @cocal/google-calendar-mcp auth
218 | 
219 | # For local installations
220 | npm run auth
221 | ```
222 | 
223 | ## License
224 | 
225 | MIT
226 | 
227 | ## Support
228 | 
229 | - [GitHub Issues](https://github.com/nspady/google-calendar-mcp/issues)
230 | - [Documentation](docs/)
231 | 
```

--------------------------------------------------------------------------------
/AGENTS.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Repository Guidelines
 2 | 
 3 | ## Project Structure & Modules
 4 | - Source: `src/` (entry `index.ts`), builds to `build/` via esbuild.
 5 | - Handlers: `src/handlers/core/` (tool implementations), utilities in `src/handlers/utils/`.
 6 | - Schemas: `src/schemas/` (Zod definitions shared between server and tests).
 7 | - Services: `src/services/` (conflict detection, helpers), transports in `src/transports/`.
 8 | - Auth: `src/auth/`, OAuth helper `src/auth-server.ts`.
 9 | - Tests: `src/tests/unit/` and `src/tests/integration/`.
10 | - Docs: `docs/` (auth, testing, deployment, architecture).
11 | 
12 | ## Build, Test, and Dev
13 | - `npm run build`: Bundle to `build/index.js` and `build/auth-server.js` (Node 18 ESM).
14 | - `npm start`: Run stdio transport (for Claude Desktop). Example: `npx @cocal/google-calendar-mcp`.
15 | - `npm run start:http`: HTTP transport on `:3000` (use `start:http:public` for `0.0.0.0`).
16 | - `npm test`: Vitest unit tests. `npm run test:integration` for Google/LLM integration.
17 | - `npm run dev`: Helper menu (auth, http, docker, targeted test runs).
18 | - `npm run auth`: Launch local OAuth flow (stores tokens in `~/.config/google-calendar-mcp`).
19 | 
20 | ## Coding Style & Naming
21 | - TypeScript, strict typing (avoid `any`). 2‑space indentation.
22 | - Files: PascalCase for handlers/services (e.g., `GetEventHandler.ts`), camelCase for functions/vars.
23 | - ESM modules with `type: module`; prefer named exports.
24 | - Validation with Zod in `src/schemas/`; validate inputs at handler boundaries.
25 | - Linting: `npm run lint` (TypeScript no‑emit checks).
26 | 
27 | ## Testing Guidelines
28 | - Framework: Vitest with V8 coverage (`npm run test:coverage`).
29 | - Unit test names: `*.test.ts` mirroring source paths (e.g., `src/tests/unit/handlers/...`).
30 | - Integration requires env: `GOOGLE_OAUTH_CREDENTIALS`, `TEST_CALENDAR_ID`; authenticate with `npm run dev auth:test`.
31 | - Use `src/tests/integration/test-data-factory.ts` utilities; ensure tests clean up created events.
32 | 
33 | ## Commit & PRs
34 | - Commits: Imperative mood, concise subject, optional scope. Examples:
35 |   - `Fix timezone handling for list-events`
36 |   - `services(conflict): improve duplicate detection`
37 | - Reference issues/PRs with `(#NN)` when applicable.
38 | - PRs: clear description, rationale, screenshots/log snippets when debugging; link issues; list notable env/config changes.
39 | - Required before PR: `npm run lint && npm test && npm run build` (and relevant integration tests if affected).
40 | 
41 | ## Security & Config
42 | - Keep credentials out of git; use `.env` and `GOOGLE_OAUTH_CREDENTIALS` path.
43 | - Test vs normal accounts controlled via `GOOGLE_ACCOUNT_MODE`; prefer `test` for integration.
44 | - Tokens stored locally in `~/.config/google-calendar-mcp/tokens.json`.
45 | 
46 | ## Adding New Tools (MCP)
47 | - Implement handler in `src/handlers/core/YourToolHandler.ts` extending `BaseToolHandler`.
48 | - Define/extend Zod schema in `src/schemas/` and add unit + integration tests.
49 | - Handlers are auto‑registered; update docs if adding public tool names.
50 | 
51 | 
```

--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------

```markdown
  1 | # CLAUDE.md
  2 | 
  3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
  4 | 
  5 | ## Overview
  6 | 
  7 | Google Calendar MCP Server - A Model Context Protocol (MCP) server providing Google Calendar integration for AI assistants. Built with TypeScript, supports both stdio and HTTP transports, with OAuth 2.0 authentication.
  8 | 
  9 | ## Development Commands
 10 | 
 11 | ```bash
 12 | npm install              # Install dependencies
 13 | npm run build            # Build with esbuild (outputs to build/)
 14 | npm run dev              # Show interactive development menu with all commands
 15 | npm run lint             # TypeScript type checking (no emit)
 16 | 
 17 | # Testing - Quick Start
 18 | npm test                             # Unit tests only (no auth required)
 19 | npm run test:watch                   # Unit tests in watch mode
 20 | npm run dev test:integration:direct  # Direct integration tests (recommended for dev)
 21 | npm run dev coverage                 # Generate test coverage report
 22 | 
 23 | # Testing - Full Suite (rarely needed, incurrs LLM usage costs)
 24 | npm run dev test:integration:claude  # Claude + MCP integration (requires CLAUDE_API_KEY)
 25 | npm run dev test:integration:openai  # OpenAI + MCP integration (requires OPENAI_API_KEY)
 26 | npm run dev test:integration:all     # All integration tests (requires all API keys)
 27 | 
 28 | # Authentication
 29 | npm run auth                # Authenticate main account
 30 | npm run dev auth:test       # Authenticate test account (for integration tests)
 31 | npm run dev account:status  # Check authentication status
 32 | 
 33 | # Running the server
 34 | npm start                        # Start with stdio transport
 35 | npm run dev http                 # Start HTTP server on localhost:3000
 36 | npm run dev http:public          # HTTP server accessible from any host
 37 | ```
 38 | 
 39 | ## Architecture
 40 | 
 41 | ### Handler Architecture
 42 | 
 43 | All MCP tools follow a consistent handler pattern:
 44 | 
 45 | 1. **Handler Registration**: Handlers are auto-registered via `src/tools/registry.ts`
 46 | 2. **Base Class**: All handlers extend `BaseToolHandler` from `src/handlers/core/BaseToolHandler.ts`
 47 | 3. **Schema Definition**: Input schemas defined in `src/tools/registry.ts` using Zod
 48 | 4. **Handler Implementation**: Core logic in `src/handlers/core/` directory
 49 | 
 50 | **Request Flow:**
 51 | ```
 52 | Client → Transport Layer → Schema Validation (Zod) → Handler → Google Calendar API → Response
 53 | ```
 54 | 
 55 | ### Adding New Tools
 56 | 
 57 | 1. Create handler class in `src/handlers/core/YourToolHandler.ts`:
 58 |    - Extend `BaseToolHandler`
 59 |    - Implement `runTool(args, oauth2Client)` method
 60 |    - Use `this.getCalendar(oauth2Client)` to get Calendar API client
 61 |    - Use `this.handleGoogleApiError(error)` for error handling
 62 | 
 63 | 2. Define schema in `src/tools/registry.ts`:
 64 |    - Add to `ToolSchemas` object with Zod schema
 65 |    - Add to `ToolRegistry.tools` array with name, description, handler class
 66 | 
 67 | 3. Add tests:
 68 |    - Unit tests in `src/tests/unit/handlers/YourToolHandler.test.ts`
 69 |    - Integration tests in `src/tests/integration/` if needed
 70 | 
 71 | **No manual registration needed** - handlers are auto-discovered by the registry system.
 72 | 
 73 | ### Authentication System
 74 | 
 75 | - **OAuth 2.0** with refresh token support
 76 | - **Multi-account**: Supports `normal` (default) and `test` accounts via `GOOGLE_ACCOUNT_MODE` env var
 77 | - **Token Storage**: `~/.config/google-calendar-mcp/tokens.json` (platform-specific paths)
 78 | - **Token Validation**: Automatic refresh on expiry
 79 | - **Components**:
 80 |   - `src/auth/client.ts` - OAuth2Client initialization
 81 |   - `src/auth/server.ts` - Auth server for OAuth flow
 82 |   - `src/auth/tokenManager.ts` - Token management and validation
 83 | 
 84 | ### Transport Layer
 85 | 
 86 | - **stdio** (default): Process communication for Claude Desktop
 87 | - **HTTP**: RESTful API with SSE for remote deployment
 88 | - **Configuration**: `src/config/TransportConfig.ts`
 89 | - **Handlers**: `src/transports/stdio.ts` and `src/transports/http.ts`
 90 | 
 91 | ### Testing Strategy
 92 | 
 93 | **Unit Tests** (`src/tests/unit/`):
 94 | - No external dependencies (mocked)
 95 | - Schema validation, error handling, datetime logic
 96 | - Run with `npm test` (no setup required)
 97 | 
 98 | **Integration Tests** (`src/tests/integration/`):
 99 | 
100 | Three types of integration tests, each with different requirements:
101 | 
102 | 1. **Direct Integration** (most commonly used):
103 |    - File: `direct-integration.test.ts`
104 |    - Tests real Google Calendar API calls
105 |    - **Setup Required**:
106 |      ```bash
107 |      # 1. Set credentials path
108 |      export GOOGLE_OAUTH_CREDENTIALS=./gcp-oauth.keys.json
109 | 
110 |      # 2. Set test calendar (use "primary" or a specific calendar ID)
111 |      export TEST_CALENDAR_ID=primary
112 | 
113 |      # 3. Authenticate test account
114 |      npm run dev auth:test
115 | 
116 |      # 4. Run tests
117 |      npm run dev test:integration:direct
118 |      ```
119 | 
120 | 2. **LLM Integration** (rarely needed):
121 |    - Files: `claude-mcp-integration.test.ts`, `openai-mcp-integration.test.ts`
122 |    - Tests end-to-end MCP protocol with AI models
123 |    - **Additional Setup** (beyond direct integration setup):
124 |      ```bash
125 |      # For Claude tests
126 |      export CLAUDE_API_KEY=sk-ant-...
127 |      npm run dev test:integration:claude
128 | 
129 |      # For OpenAI tests
130 |      export OPENAI_API_KEY=sk-...
131 |      npm run dev test:integration:openai
132 | 
133 |      # For both
134 |      npm run dev test:integration:all
135 |      ```
136 |    - ⚠️ Consumes API credits and takes 2-5 minutes
137 | 
138 | **Quick Setup Summary:**
139 | ```bash
140 | # Minimal setup for development (direct integration tests only):
141 | export GOOGLE_OAUTH_CREDENTIALS=./gcp-oauth.keys.json
142 | export TEST_CALENDAR_ID=primary
143 | npm run dev auth:test
144 | npm run dev test:integration:direct
145 | ```
146 | 
147 | ### Key Services
148 | 
149 | **Conflict Detection** (`src/services/conflict-detection/`):
150 | - `ConflictAnalyzer.ts` - Detects scheduling conflicts
151 | - `EventSimilarityChecker.ts` - Identifies duplicate events
152 | - `ConflictDetectionService.ts` - Main service coordinating conflict checks
153 | - Used by `create-event` and `update-event` handlers
154 | 
155 | **Structured Responses** (`src/types/structured-responses.ts`):
156 | - TypeScript interfaces for consistent response formats
157 | - Used across handlers for type safety
158 | 
159 | **Utilities**:
160 | - `src/utils/field-mask-builder.ts` - Builds Google API field masks
161 | - `src/utils/event-id-validator.ts` - Validates Google Calendar event IDs
162 | - `src/utils/response-builder.ts` - Formats MCP responses
163 | - `src/handlers/utils/datetime.ts` - Timezone and datetime utilities
164 | 
165 | ## Important Patterns
166 | 
167 | ### Timezone Handling
168 | 
169 | - **Preferred Format**: ISO 8601 without timezone (e.g., `2024-01-01T10:00:00`)
170 |   - Uses `timeZone` parameter or calendar's default timezone
171 | - **Also Supported**: ISO 8601 with timezone (e.g., `2024-01-01T10:00:00-08:00`)
172 | - **All-day Events**: Date only format (e.g., `2024-01-01`)
173 | - **Helper**: `getCalendarTimezone()` method in `BaseToolHandler`
174 | 
175 | ### Multi-Calendar Support
176 | 
177 | - `list-events` accepts single calendar ID or JSON array: `'["cal1", "cal2"]'`
178 | - Batch requests handled by `BatchRequestHandler.ts`
179 | - Maximum 50 calendars per request
180 | 
181 | ### Recurring Events
182 | 
183 | - Modification scopes: `thisEventOnly`, `thisAndFollowing`, `all`
184 | - Handled by `RecurringEventHelpers.ts`
185 | - Special validation in `update-event` schema
186 | 
187 | ### Error Handling
188 | 
189 | - Use `McpError` from `@modelcontextprotocol/sdk/types.js`
190 | - `BaseToolHandler.handleGoogleApiError()` for consistent Google API error handling
191 | - Maps HTTP status codes to appropriate MCP error codes
192 | 
193 | ### Structured Output Migration
194 | 
195 | The codebase uses a structured response format for tool outputs. Recent commits (see git status) show migration to structured outputs using types from `src/types/structured-responses.ts`. When updating handlers, ensure responses conform to these structured formats.
196 | 
197 | ### MCP Structure
198 | 
199 | MCP tools return errors as successful responses with error content, not as thrown exceptions. Integration tests must validate result.content[0].text for error messages, while unit tests of handlers directly can still catch thrown McpError exceptions before the MCP transport layer wraps them.
200 | 
201 | ## Code Quality
202 | 
203 | - **TypeScript**: Strict mode, avoid `any` types
204 | - **Formatting**: Use existing patterns in handlers
205 | - **Testing**: Add unit tests for all new handlers
206 | - **Error Messages**: Clear, actionable error messages referencing Google Calendar concepts
207 | 
208 | ## Google Calendar API
209 | 
210 | - **Version**: v3 (`googleapis` package)
211 | - **Timeout**: 3 seconds per API call (configured in `BaseToolHandler`)
212 | - **Rate Limiting**: Google Calendar API has quotas - integration tests may hit limits
213 | - **Scopes Required**:
214 |   - `https://www.googleapis.com/auth/calendar.events`
215 |   - `https://www.googleapis.com/auth/calendar`
216 | 
217 | ## Deployment
218 | 
219 | - **npx**: `npx @cocal/google-calendar-mcp` (requires `GOOGLE_OAUTH_CREDENTIALS` env var)
220 | - **Docker**: See `docs/docker.md` for Docker deployment with stdio and HTTP modes
221 | - **Claude Desktop Config**: See README.md for local stdio configuration
222 | 
223 | ### Deployment Modes
224 | 
225 | **Local Development (Claude Desktop):**
226 | - Use **stdio mode** (default)
227 | - No server or domain required
228 | - Direct process communication
229 | - See README.md for setup
230 | 
231 | **Key Differences:**
232 | - **stdio**: For Claude Desktop only, local machine
233 | - **HTTP**: For testing, development, debugging (local only)
234 | 
235 | 
```

--------------------------------------------------------------------------------
/src/auth/paths.d.ts:
--------------------------------------------------------------------------------

```typescript
1 | export function getSecureTokenPath(): string;
2 | export function getLegacyTokenPath(): string;
3 | export function getAccountMode(): 'normal' | 'test';
4 | 
5 | 
```

--------------------------------------------------------------------------------
/tsconfig.lint.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "extends": "./tsconfig.json",
 3 |   "include": [
 4 |     "src/**/*.ts"
 5 |   ],
 6 |   "exclude": [
 7 |     "src/tests/**"
 8 |   ],
 9 |   "compilerOptions": {
10 |     "noEmit": true,
11 |     "allowJs": true
12 |   }
13 | }
14 | 
15 | 
```

--------------------------------------------------------------------------------
/gcp-oauth.keys.example.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |     "installed": {
3 |         "client_id": "YOUR_GOOGLE_CLIENT_ID",
4 |         "client_secret": "YOUR_GOOGLE_CLIENT_SECRET",
5 |         "redirect_uris": ["http://localhost:3000/oauth2callback"]
6 |     }
7 | }
8 | 
```

--------------------------------------------------------------------------------
/src/services/conflict-detection/index.ts:
--------------------------------------------------------------------------------

```typescript
1 | export { ConflictDetectionService } from './ConflictDetectionService.js';
2 | export { EventSimilarityChecker } from './EventSimilarityChecker.js';
3 | export { ConflictAnalyzer } from './ConflictAnalyzer.js';
4 | export * from './types.js';
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true,
12 |     "resolveJsonModule": true,
13 |     "types": ["node"]
14 |   },
15 |   "include": ["src/**/*"],
16 |   "exclude": ["node_modules"]
17 | }
18 | 
```

--------------------------------------------------------------------------------
/src/transports/stdio.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 3 | 
 4 | export class StdioTransportHandler {
 5 |   private server: McpServer;
 6 | 
 7 |   constructor(server: McpServer) {
 8 |     this.server = server;
 9 |   }
10 | 
11 |   async connect(): Promise<void> {
12 |     const transport = new StdioServerTransport();
13 |     await this.server.connect(transport);
14 |   }
15 | } 
```

--------------------------------------------------------------------------------
/src/services/conflict-detection/config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Centralized configuration for conflict detection thresholds
 3 |  */
 4 | 
 5 | export const CONFLICT_DETECTION_CONFIG = {
 6 |   /**
 7 |    * Thresholds for duplicate event detection
 8 |    */
 9 |   DUPLICATE_THRESHOLDS: {
10 |     /**
11 |      * Events with similarity >= this value are flagged as potential duplicates
12 |      * and shown as warnings during creation
13 |      */
14 |     WARNING: 0.7,
15 |     
16 |     /**
17 |      * Events with similarity >= this value are considered exact duplicates
18 |      * and block creation unless explicitly overridden with allowDuplicates flag
19 |      */
20 |     BLOCKING: 0.95
21 |   },
22 |   
23 |   /**
24 |    * Default similarity threshold for duplicate detection
25 |    * Used when duplicateSimilarityThreshold is not specified in the request
26 |    */
27 |   DEFAULT_DUPLICATE_THRESHOLD: 0.7
28 | } as const;
29 | 
30 | export type ConflictDetectionConfig = typeof CONFLICT_DETECTION_CONFIG;
```

--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { defineConfig } from 'vitest/config'
 2 | import { loadEnv } from 'vite'
 3 | 
 4 | export default defineConfig({
 5 |   test: {
 6 |     globals: true, // Use Vitest globals (describe, it, expect) like Jest
 7 |     environment: 'node', // Specify the test environment
 8 |     // Load environment variables from .env file
 9 |     env: loadEnv('', process.cwd(), ''),
10 |     // Increase timeout for AI API calls
11 |     testTimeout: 30000,
12 |     include: [
13 |       'src/tests/**/*.test.ts'
14 |     ],
15 |     // Exclude integration tests by default (they require credentials)
16 |     exclude: ['**/node_modules/**'],
17 |     // Enable coverage
18 |     coverage: {
19 |       provider: 'v8', // or 'istanbul'
20 |       reporter: ['text', 'json', 'html'],
21 |       exclude: [
22 |         '**/node_modules/**',
23 |         'src/tests/integration/**',
24 |         'build/**',
25 |         'scripts/**',
26 |         '*.config.*'
27 |       ],
28 |     },
29 |   },
30 | }) 
```

--------------------------------------------------------------------------------
/docs/development.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Development Guide
 2 | 
 3 | ## Setup
 4 | 
 5 | ```bash
 6 | git clone https://github.com/nspady/google-calendar-mcp.git
 7 | cd google-calendar-mcp
 8 | npm install
 9 | npm run build
10 | npm run auth                # Authenticate main account
11 | npm run dev auth:test       # Authenticate test account (used for integration tests) 
12 | ```
13 | 
14 | ## Development
15 | 
16 | ```bash
17 | npm run dev         # Interactive development menu
18 | npm run build       # Build project  
19 | npm run lint        # Type-check with TypeScript (no emit)
20 | npm test            # Run tests
21 | ```
22 | 
23 | ## Contributing
24 | 
25 | - Follow existing code patterns
26 | - Add tests for new features  
27 | - Use TypeScript strictly (avoid `any`)
28 | - Run `npm run dev` for development tools
29 | 
30 | ## Adding New Tools
31 | 
32 | 1. Create handler in `src/handlers/core/NewToolHandler.ts`
33 | 2. Define schema in `src/schemas/`  
34 | 3. Add tests in `src/tests/`
35 | 4. Auto-discovered by registry system
36 | 
37 | See existing handlers for patterns.
38 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Google Calendar MCP Server - Optimized Dockerfile
 2 | # syntax=docker/dockerfile:1
 3 | 
 4 | FROM node:18-alpine
 5 | 
 6 | # Create app user for security
 7 | RUN addgroup -g 1001 -S nodejs && \
 8 |     adduser -S -u 1001 -G nodejs nodejs
 9 | 
10 | # Set working directory
11 | WORKDIR /app
12 | 
13 | # Copy package files for dependency caching
14 | COPY package*.json ./
15 | 
16 | # Copy build scripts and source files needed for build
17 | COPY scripts ./scripts
18 | COPY src ./src
19 | COPY tsconfig.json .
20 | 
21 | # Install all dependencies (including dev dependencies for build)
22 | RUN npm ci --no-audit --no-fund --silent
23 | 
24 | # Build the project
25 | RUN npm run build
26 | 
27 | # Remove dev dependencies to reduce image size
28 | RUN npm prune --production --silent
29 | 
30 | # Create config directory and set permissions
31 | RUN mkdir -p /home/nodejs/.config/google-calendar-mcp && \
32 |     chown -R nodejs:nodejs /home/nodejs/.config && \
33 |     chown -R nodejs:nodejs /app
34 | 
35 | # Switch to non-root user
36 | USER nodejs
37 | 
38 | # Expose port for HTTP mode (optional)
39 | EXPOSE 3000
40 | 
41 | # Default command - run directly to avoid npm output
42 | CMD ["node", "build/index.js"]
```

--------------------------------------------------------------------------------
/src/auth/paths.js:
--------------------------------------------------------------------------------

```javascript
 1 | #!/usr/bin/env node
 2 | 
 3 | /**
 4 |  * Shared path utilities for token management
 5 |  * This module provides consistent token path resolution across all scripts
 6 |  */
 7 | 
 8 | import path from 'path';
 9 | import { homedir } from 'os';
10 | 
11 | /**
12 |  * Get the secure token storage path
13 |  * Uses XDG Base Directory specification on Unix-like systems
14 |  */
15 | export function getSecureTokenPath() {
16 |   const configDir = process.env.XDG_CONFIG_HOME || path.join(homedir(), '.config');
17 |   return path.join(configDir, 'google-calendar-mcp', 'tokens.json');
18 | }
19 | 
20 | /**
21 |  * Get the legacy token path (for migration purposes)
22 |  */
23 | export function getLegacyTokenPath() {
24 |   return path.join(process.cwd(), '.gcp-saved-tokens.json');
25 | }
26 | 
27 | /**
28 |  * Get current account mode from environment
29 |  * Uses same logic as utils.ts but compatible with both JS and TS
30 |  */
31 | export function getAccountMode() {
32 |   // If set explicitly via environment variable use that instead
33 |   const explicitMode = process.env.GOOGLE_ACCOUNT_MODE?.toLowerCase();
34 |   if (explicitMode === 'test' || explicitMode === 'normal') {
35 |     return explicitMode;
36 |   }
37 |   
38 |   // Auto-detect test environment
39 |   if (process.env.NODE_ENV === 'test') {
40 |     return 'test';
41 |   }
42 |   
43 |   // Default to normal for regular app usage
44 |   return 'normal';
45 | }
```

--------------------------------------------------------------------------------
/src/schemas/types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // TypeScript interfaces for Google Calendar data structures
 2 | 
 3 | export interface CalendarListEntry {
 4 |   id?: string | null;
 5 |   summary?: string | null;
 6 | }
 7 | 
 8 | export interface CalendarEventReminder {
 9 |   method: 'email' | 'popup';
10 |   minutes: number;
11 | }
12 | 
13 | export interface CalendarEventAttendee {
14 |   email?: string | null;
15 |   responseStatus?: string | null;
16 | }
17 | 
18 | export interface CalendarEvent {
19 |   id?: string | null;
20 |   summary?: string | null;
21 |   start?: {
22 |     dateTime?: string | null;
23 |     date?: string | null;
24 |     timeZone?: string | null;
25 |   };
26 |   end?: {
27 |     dateTime?: string | null;
28 |     date?: string | null;
29 |     timeZone?: string | null;
30 |   };
31 |   location?: string | null;
32 |   attendees?: CalendarEventAttendee[] | null;
33 |   colorId?: string | null;
34 |   reminders?: {
35 |     useDefault: boolean;
36 |     overrides?: CalendarEventReminder[];
37 |   };
38 |   recurrence?: string[] | null;
39 | }
40 | 
41 | // Type-safe response based on Google Calendar FreeBusy API
42 | export interface FreeBusyResponse {
43 |   kind: "calendar#freeBusy";
44 |   timeMin: string;
45 |   timeMax: string;
46 |   groups?: {
47 |     [key: string]: {
48 |       errors?: { domain: string; reason: string }[];
49 |       calendars?: string[];
50 |     };
51 |   };
52 |   calendars: {
53 |     [key: string]: {
54 |       errors?: { domain: string; reason: string }[];
55 |       busy: {
56 |         start: string;
57 |         end: string;
58 |       }[];
59 |     };
60 |   };
61 | }
```

--------------------------------------------------------------------------------
/src/handlers/core/DeleteEventHandler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
 2 | import { OAuth2Client } from "google-auth-library";
 3 | import { BaseToolHandler } from "./BaseToolHandler.js";
 4 | import { DeleteEventInput } from "../../tools/registry.js";
 5 | import { DeleteEventResponse } from "../../types/structured-responses.js";
 6 | import { createStructuredResponse } from "../../utils/response-builder.js";
 7 | 
 8 | export class DeleteEventHandler extends BaseToolHandler {
 9 |     async runTool(args: any, oauth2Client: OAuth2Client): Promise<CallToolResult> {
10 |         const validArgs = args as DeleteEventInput;
11 |         await this.deleteEvent(oauth2Client, validArgs);
12 | 
13 |         const response: DeleteEventResponse = {
14 |             success: true,
15 |             eventId: validArgs.eventId,
16 |             calendarId: validArgs.calendarId,
17 |             message: "Event deleted successfully"
18 |         };
19 | 
20 |         return createStructuredResponse(response);
21 |     }
22 | 
23 |     private async deleteEvent(
24 |         client: OAuth2Client,
25 |         args: DeleteEventInput
26 |     ): Promise<void> {
27 |         try {
28 |             const calendar = this.getCalendar(client);
29 |             await calendar.events.delete({
30 |                 calendarId: args.calendarId,
31 |                 eventId: args.eventId,
32 |                 sendUpdates: args.sendUpdates,
33 |             });
34 |         } catch (error) {
35 |             throw this.handleGoogleApiError(error);
36 |         }
37 |     }
38 | }
39 | 
```

--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # Google Calendar MCP Server - Docker Compose Configuration
 2 | # Simple, production-ready setup following Docker best practices
 3 | 
 4 | services:
 5 |   calendar-mcp:
 6 |     build: .
 7 |     container_name: calendar-mcp
 8 |     restart: unless-stopped
 9 |     
10 |     # Environment configuration via .env file
11 |     env_file: .env
12 | 
13 |     # OAuth credentials and token storage
14 |     volumes:
15 |       - ./gcp-oauth.keys.json:/app/gcp-oauth.keys.json:ro
16 |       - calendar-tokens:/home/nodejs/.config/google-calendar-mcp
17 |     
18 |     # Expose ports for HTTP mode and OAuth authentication
19 |     ports:
20 |       - "3000:3000"  # HTTP mode MCP server
21 |       - "3500:3500"  # OAuth authentication
22 |       - "3501:3501"
23 |       - "3502:3502"
24 |       - "3503:3503"
25 |       - "3504:3504"
26 |       - "3505:3505"
27 |     
28 |     
29 |     # Resource limits for production stability
30 |     deploy:
31 |       resources:
32 |         limits:
33 |           memory: 512M
34 |           cpus: "1.0"
35 |         reservations:
36 |           memory: 256M
37 |           cpus: "0.5"
38 |     
39 |     # Security options
40 |     security_opt:
41 |       - no-new-privileges:true
42 |     
43 |     # Health check for HTTP mode (safe for stdio mode too)
44 |     healthcheck:
45 |       test: ["CMD-SHELL", "if [ \"$TRANSPORT\" = \"http\" ]; then curl -f http://localhost:${PORT:-3000}/health || exit 1; else exit 0; fi"]
46 |       interval: 30s
47 |       timeout: 10s
48 |       retries: 3
49 |       start_period: 40s
50 | 
51 | # Persistent volume for OAuth tokens
52 | volumes:
53 |   calendar-tokens:
54 |     driver: local
```

--------------------------------------------------------------------------------
/src/services/conflict-detection/types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { calendar_v3 } from "googleapis";
 2 | 
 3 | /**
 4 |  * Internal conflict info used by the conflict detection service.
 5 |  * Contains additional internal fields not exposed in public API responses.
 6 |  */
 7 | export interface InternalConflictInfo {
 8 |   type: 'overlap' | 'duplicate';
 9 |   calendar: string;
10 |   event: {
11 |     id: string;
12 |     title: string;
13 |     url?: string;
14 |     start?: string;
15 |     end?: string;
16 |   };
17 |   fullEvent?: calendar_v3.Schema$Event;
18 |   overlap?: {
19 |     duration: string;
20 |     percentage: number;
21 |     startTime: string;
22 |     endTime: string;
23 |   };
24 |   similarity?: number;
25 | }
26 | 
27 | /**
28 |  * Internal duplicate info used by the conflict detection service.
29 |  * Contains additional internal fields not exposed in public API responses.
30 |  */
31 | export interface InternalDuplicateInfo {
32 |   event: {
33 |     id: string;
34 |     title: string;
35 |     start?: string;
36 |     end?: string;
37 |     url?: string;
38 |     similarity: number;
39 |   };
40 |   fullEvent?: calendar_v3.Schema$Event;
41 |   calendarId?: string;
42 |   suggestion: string;
43 | }
44 | 
45 | export interface ConflictCheckResult {
46 |   hasConflicts: boolean;
47 |   conflicts: InternalConflictInfo[];
48 |   duplicates: InternalDuplicateInfo[];
49 | }
50 | 
51 | export interface EventTimeRange {
52 |   start: Date;
53 |   end: Date;
54 |   isAllDay: boolean;
55 | }
56 | 
57 | export interface ConflictDetectionOptions {
58 |   checkDuplicates?: boolean;
59 |   checkConflicts?: boolean;
60 |   calendarsToCheck?: string[];
61 |   duplicateSimilarityThreshold?: number;
62 |   includeDeclinedEvents?: boolean;
63 | }
```

--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Release
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - main
 7 | 
 8 | permissions:
 9 |   contents: write
10 |   pull-requests: write
11 |   id-token: write
12 | 
13 | jobs:
14 |   release-please:
15 |     runs-on: ubuntu-latest
16 |     steps:
17 |       - uses: googleapis/release-please-action@v4
18 |         id: release
19 |         with:
20 |           release-type: node
21 | 
22 |       # Only run the following steps if a release was created
23 |       - uses: actions/checkout@v4
24 |         if: ${{ steps.release.outputs.release_created }}
25 | 
26 |       - uses: actions/setup-node@v4
27 |         if: ${{ steps.release.outputs.release_created }}
28 |         with:
29 |           node-version: '18'
30 |           registry-url: 'https://registry.npmjs.org'
31 | 
32 |       - name: Install dependencies
33 |         if: ${{ steps.release.outputs.release_created }}
34 |         run: npm ci
35 | 
36 |       - name: Build project
37 |         if: ${{ steps.release.outputs.release_created }}
38 |         run: npm run build
39 | 
40 |       - name: Publish to NPM
41 |         if: ${{ steps.release.outputs.release_created }}
42 |         run: |
43 |           VERSION="${{ steps.release.outputs.tag_name }}"
44 | 
45 |           # Check if this is a prerelease version (contains -, like v1.3.0-beta.0)
46 |           if [[ "$VERSION" == *"-"* ]]; then
47 |             echo "Publishing prerelease version $VERSION with beta tag"
48 |             npm publish --provenance --access public --tag beta
49 |           else
50 |             echo "Publishing stable version $VERSION with latest tag"
51 |             npm publish --provenance --access public
52 |           fi
53 |         env:
54 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
55 | 
```

--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------

```javascript
 1 | #!/usr/bin/env node
 2 | 
 3 | import * as esbuild from 'esbuild';
 4 | import { fileURLToPath } from 'url';
 5 | import { dirname, join } from 'path';
 6 | 
 7 | const __dirname = dirname(fileURLToPath(import.meta.url));
 8 | const isWatch = process.argv.includes('--watch');
 9 | 
10 | /** @type {import('esbuild').BuildOptions} */
11 | const buildOptions = {
12 |   entryPoints: [join(__dirname, '../src/index.ts')],
13 |   bundle: true,
14 |   platform: 'node',
15 |   target: 'node18',
16 |   outfile: join(__dirname, '../build/index.js'),
17 |   format: 'esm',
18 |   banner: {
19 |     js: '#!/usr/bin/env node\n',
20 |   },
21 |   packages: 'external', // Don't bundle node_modules
22 |   sourcemap: true,
23 | };
24 | 
25 | /** @type {import('esbuild').BuildOptions} */
26 | const authServerBuildOptions = {
27 |   entryPoints: [join(__dirname, '../src/auth-server.ts')],
28 |   bundle: true,
29 |   platform: 'node',
30 |   target: 'node18',
31 |   outfile: join(__dirname, '../build/auth-server.js'),
32 |   format: 'esm',
33 |   packages: 'external', // Don't bundle node_modules
34 |   sourcemap: true,
35 | };
36 | 
37 | if (isWatch) {
38 |   const context = await esbuild.context(buildOptions);
39 |   const authContext = await esbuild.context(authServerBuildOptions);
40 |   await Promise.all([context.watch(), authContext.watch()]);
41 |   process.stderr.write('Watching for changes...\n');
42 | } else {
43 |   await Promise.all([
44 |     esbuild.build(buildOptions),
45 |     esbuild.build(authServerBuildOptions)
46 |   ]);
47 |   
48 |   // Make the file executable on non-Windows platforms
49 |   if (process.platform !== 'win32') {
50 |     const { chmod } = await import('fs/promises');
51 |     await chmod(buildOptions.outfile, 0o755);
52 |   }
53 | } 
```

--------------------------------------------------------------------------------
/docs/architecture.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Architecture Overview
 2 | 
 3 | ## Transport Layer
 4 | 
 5 | - **stdio** (default): Direct process communication for Claude Desktop
 6 | - **HTTP**: RESTful API with SSE for remote deployment
 7 | 
 8 | ## Authentication System
 9 | 
10 | OAuth 2.0 with refresh tokens, multi-account support, secure storage in `~/.config/google-calendar-mcp/tokens.json`.
11 | 
12 | ## Handler Architecture
13 | 
14 | - `src/handlers/core/` - Individual tool handlers extending `BaseToolHandler`
15 | - `src/tools/registry.ts` - Auto-registration system discovers and registers handlers
16 | - `src/schemas/` - Input validation and type definitions
17 | 
18 | ## Request Flow
19 | 
20 | ```
21 | Client → Transport → Schema Validation → Handler → Google API → Response
22 | ```
23 | 
24 | ## MCP Tools
25 | 
26 | The server provides calendar management tools that LLMs can use for calendar operations:
27 | 
28 | ### Available Tools
29 | 
30 | - `list-calendars` - List all available calendars
31 | - `list-events` - List events with date filtering  
32 | - `search-events` - Search events by text query
33 | - `create-event` - Create new calendar events
34 | - `update-event` - Update existing events
35 | - `delete-event` - Delete events
36 | - `get-freebusy` - Check availability across calendars
37 | - `list-colors` - List available event colors
38 | - `get-current-time` - Get current system time and timezone information
39 | 
40 | ## Key Features
41 | 
42 | - **Auto-registration**: Handlers automatically discovered
43 | - **Multi-account**: Normal/test account support  
44 | - **Rate limiting**: Respects Google Calendar quotas
45 | - **Batch operations**: Efficient multi-calendar queries
46 | - **Recurring events**: Advanced modification scopes
47 | - **Contextual resources**: Real-time date/time information
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@cocal/google-calendar-mcp",
 3 |   "version": "2.0.6",
 4 |   "description": "Google Calendar MCP Server with extensive support for calendar management",
 5 |   "type": "module",
 6 |   "main": "build/index.js",
 7 |   "bin": {
 8 |     "google-calendar-mcp": "build/index.js"
 9 |   },
10 |   "files": [
11 |     "build/",
12 |     "README.md",
13 |     "LICENSE"
14 |   ],
15 |   "keywords": [
16 |     "mcp",
17 |     "model-context-protocol",
18 |     "claude",
19 |     "google-calendar",
20 |     "calendar",
21 |     "ai",
22 |     "llm",
23 |     "integration"
24 |   ],
25 |   "repository": {
26 |     "type": "git",
27 |     "url": "git+https://github.com/nspady/google-calendar-mcp.git"
28 |   },
29 |   "bugs": {
30 |     "url": "https://github.com/nspady/google-calendar-mcp/issues"
31 |   },
32 |   "homepage": "https://github.com/nspady/google-calendar-mcp#readme",
33 |   "author": "nspady",
34 |   "license": "MIT",
35 |   "scripts": {
36 |     "start": "node build/index.js",
37 |     "build": "node scripts/build.js",
38 |     "auth": "node build/auth-server.js",
39 |     "dev": "node scripts/dev.js",
40 |     "lint": "tsc --noEmit -p tsconfig.lint.json",
41 |     "test": "vitest run src/tests/unit",
42 |     "test:watch": "vitest src/tests/unit",
43 |     "test:integration": "vitest run src/tests/integration",
44 |     "test:all": "vitest run src/tests",
45 |     "test:coverage": "vitest run src/tests/unit --coverage",
46 |     "start:http": "node build/index.js --transport http --port 3000",
47 |     "start:http:public": "node build/index.js --transport http --port 3000 --host 0.0.0.0"
48 |   },
49 |   "dependencies": {
50 |     "@google-cloud/local-auth": "^3.0.1",
51 |     "@modelcontextprotocol/sdk": "^1.12.1",
52 |     "google-auth-library": "^9.15.0",
53 |     "googleapis": "^144.0.0",
54 |     "open": "^7.4.2",
55 |     "zod": "^3.22.4",
56 |     "zod-to-json-schema": "^3.24.5"
57 |   },
58 |   "devDependencies": {
59 |     "@anthropic-ai/sdk": "^0.52.0",
60 |     "@types/node": "^20.10.4",
61 |     "@vitest/coverage-v8": "^3.1.1",
62 |     "esbuild": "^0.25.0",
63 |     "openai": "^4.104.0",
64 |     "typescript": "^5.3.3",
65 |     "vitest": "^3.1.1"
66 |   }
67 | }
68 | 
```

--------------------------------------------------------------------------------
/src/config/TransportConfig.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export interface TransportConfig {
 2 |   type: 'stdio' | 'http';
 3 |   port?: number;
 4 |   host?: string;
 5 | }
 6 | 
 7 | export interface ServerConfig {
 8 |   transport: TransportConfig;
 9 |   debug?: boolean;
10 | }
11 | 
12 | export function parseArgs(args: string[]): ServerConfig {
13 |   // Start with environment variables as base config
14 |   const config: ServerConfig = {
15 |     transport: {
16 |       type: (process.env.TRANSPORT as 'stdio' | 'http') || 'stdio',
17 |       port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
18 |       host: process.env.HOST || '127.0.0.1'
19 |     },
20 |     debug: process.env.DEBUG === 'true' || false
21 |   };
22 | 
23 |   for (let i = 0; i < args.length; i++) {
24 |     const arg = args[i];
25 |     
26 |     switch (arg) {
27 |       case '--transport':
28 |         const transport = args[++i];
29 |         if (transport === 'stdio' || transport === 'http') {
30 |           config.transport.type = transport;
31 |         }
32 |         break;
33 |       case '--port':
34 |         config.transport.port = parseInt(args[++i], 10);
35 |         break;
36 |       case '--host':
37 |         config.transport.host = args[++i];
38 |         break;
39 |       case '--debug':
40 |         config.debug = true;
41 |         break;
42 |       case '--help':
43 |         process.stderr.write(`
44 | Google Calendar MCP Server
45 | 
46 | Usage: node build/index.js [options]
47 | 
48 | Options:
49 |   --transport <type>        Transport type: stdio (default) | http
50 |   --port <number>          Port for HTTP transport (default: 3000)
51 |   --host <string>          Host for HTTP transport (default: 127.0.0.1)
52 |   --debug                  Enable debug logging
53 |   --help                   Show this help message
54 | 
55 | Environment Variables:
56 |   TRANSPORT               Transport type: stdio | http
57 |   PORT                   Port for HTTP transport
58 |   HOST                   Host for HTTP transport
59 |   DEBUG                  Enable debug logging (true/false)
60 | 
61 | Examples:
62 |   node build/index.js                              # stdio (local use)
63 |   node build/index.js --transport http --port 3000 # HTTP server
64 |   PORT=3000 TRANSPORT=http node build/index.js     # Using env vars
65 |         `);
66 |         process.exit(0);
67 |     }
68 |   }
69 | 
70 |   return config;
71 | } 
```

--------------------------------------------------------------------------------
/src/handlers/core/GetEventHandler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
 2 | import { OAuth2Client } from "google-auth-library";
 3 | import { BaseToolHandler } from "./BaseToolHandler.js";
 4 | import { calendar_v3 } from 'googleapis';
 5 | import { buildSingleEventFieldMask } from "../../utils/field-mask-builder.js";
 6 | import { createStructuredResponse } from "../../utils/response-builder.js";
 7 | import { GetEventResponse, convertGoogleEventToStructured } from "../../types/structured-responses.js";
 8 | 
 9 | interface GetEventArgs {
10 |     calendarId: string;
11 |     eventId: string;
12 |     fields?: string[];
13 | }
14 | 
15 | export class GetEventHandler extends BaseToolHandler {
16 |     async runTool(args: GetEventArgs, oauth2Client: OAuth2Client): Promise<CallToolResult> {
17 |         const validArgs = args;
18 |         
19 |         try {
20 |             const event = await this.getEvent(oauth2Client, validArgs);
21 |             
22 |             if (!event) {
23 |                 throw new Error(`Event with ID '${validArgs.eventId}' not found in calendar '${validArgs.calendarId}'.`);
24 |             }
25 |             
26 |             const response: GetEventResponse = {
27 |                 event: convertGoogleEventToStructured(event, validArgs.calendarId)
28 |             };
29 |             
30 |             return createStructuredResponse(response);
31 |         } catch (error) {
32 |             throw this.handleGoogleApiError(error);
33 |         }
34 |     }
35 | 
36 |     private async getEvent(
37 |         client: OAuth2Client,
38 |         args: GetEventArgs
39 |     ): Promise<calendar_v3.Schema$Event | null> {
40 |         const calendar = this.getCalendar(client);
41 |         
42 |         const fieldMask = buildSingleEventFieldMask(args.fields);
43 |         
44 |         try {
45 |             const response = await calendar.events.get({
46 |                 calendarId: args.calendarId,
47 |                 eventId: args.eventId,
48 |                 ...(fieldMask && { fields: fieldMask })
49 |             });
50 |             
51 |             return response.data;
52 |         } catch (error: any) {
53 |             // Handle 404 as a not found case
54 |             if (error?.code === 404 || error?.response?.status === 404) {
55 |                 return null;
56 |             }
57 |             throw error;
58 |         }
59 |     }
60 | }
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ main, 'feature/**' ]
 6 |   pull_request:
 7 |     branches: [ main ]
 8 | 
 9 | jobs:
10 |   # Job 1: Test and Coverage (includes build, quality checks, and coverage reporting)
11 |   test-and-coverage:
12 |     runs-on: ubuntu-latest
13 |     
14 |     strategy:
15 |       matrix:
16 |         node-version: ['18']
17 |     
18 |     steps:
19 |     - name: Checkout code
20 |       uses: actions/checkout@v4
21 |       
22 |     - name: Setup Node.js ${{ matrix.node-version }}
23 |       uses: actions/setup-node@v4
24 |       with:
25 |         node-version: ${{ matrix.node-version }}
26 |         cache: 'npm'
27 |         
28 |     - name: Install dependencies
29 |       run: npm ci
30 |       
31 |     - name: Run unit tests with coverage
32 |       run: |
33 |         echo "🧪 Running unit tests with coverage..."
34 |         echo "======================================"
35 |         npm run test:coverage | tee test-output.log
36 |         
37 |         echo ""
38 |         echo "📊 Test Results Summary:"
39 |         echo "======================="
40 |         
41 |         # Extract test results from the log
42 |         TEST_FILES=$(grep "Test Files" test-output.log | tail -1 | sed 's/.*Test Files[[:space:]]*//' | sed 's/[[:space:]]*passed.*//')
43 |         TESTS_PASSED=$(grep "Tests" test-output.log | tail -1 | sed 's/.*Tests[[:space:]]*//' | sed 's/[[:space:]]*passed.*//')
44 |         
45 |         echo "✅ Test Files: $TEST_FILES passed"
46 |         echo "✅ Total Tests: $TESTS_PASSED passed"
47 |         echo "📁 Unit Test Files: $(find src/tests/unit -name '*.test.ts' | wc -l | tr -d ' ') files"
48 |         echo "📁 Source Files: $(find src -name '*.ts' -not -path 'src/tests/*' | wc -l | tr -d ' ') files"
49 |         
50 |         echo ""
51 |         echo "📈 Coverage Summary (from detailed report above):"
52 |         echo "================================================="
53 |         echo "• Lines, Functions, Branches, and Statements coverage shown in table above"
54 |         echo "• Full HTML report available in coverage/index.html artifact"
55 |         
56 |         # Clean up temp file
57 |         rm -f test-output.log
58 |       env:
59 |         NODE_ENV: test
60 |         
61 |     - name: Upload coverage reports
62 |       uses: actions/upload-artifact@v4
63 |       with:
64 |         name: coverage-report
65 |         path: coverage/
66 |         retention-days: 30
67 | 
68 | 
```

--------------------------------------------------------------------------------
/src/handlers/core/ListColorsHandler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
 2 | import { OAuth2Client } from "google-auth-library";
 3 | import { BaseToolHandler } from "./BaseToolHandler.js";
 4 | import { calendar_v3 } from "googleapis";
 5 | import { createStructuredResponse } from "../../utils/response-builder.js";
 6 | import { ListColorsResponse } from "../../types/structured-responses.js";
 7 | 
 8 | export class ListColorsHandler extends BaseToolHandler {
 9 |     async runTool(_: any, oauth2Client: OAuth2Client): Promise<CallToolResult> {
10 |         const colors = await this.listColors(oauth2Client);
11 |         
12 |         const response: ListColorsResponse = {
13 |             event: {},
14 |             calendar: {}
15 |         };
16 |         
17 |         // Convert event colors
18 |         if (colors.event) {
19 |             for (const [id, color] of Object.entries(colors.event)) {
20 |                 response.event[id] = {
21 |                     background: color.background || '',
22 |                     foreground: color.foreground || ''
23 |                 };
24 |             }
25 |         }
26 |         
27 |         // Convert calendar colors
28 |         if (colors.calendar) {
29 |             for (const [id, color] of Object.entries(colors.calendar)) {
30 |                 response.calendar[id] = {
31 |                     background: color.background || '',
32 |                     foreground: color.foreground || ''
33 |                 };
34 |             }
35 |         }
36 |         
37 |         return createStructuredResponse(response);
38 |     }
39 | 
40 |     private async listColors(client: OAuth2Client): Promise<calendar_v3.Schema$Colors> {
41 |         try {
42 |             const calendar = this.getCalendar(client);
43 |             const response = await calendar.colors.get();
44 |             if (!response.data) throw new Error('Failed to retrieve colors');
45 |             return response.data;
46 |         } catch (error) {
47 |             throw this.handleGoogleApiError(error);
48 |         }
49 |     }
50 | 
51 |     /**
52 |      * Formats the color information into a user-friendly string.
53 |      */
54 |     private formatColorList(colors: calendar_v3.Schema$Colors): string {
55 |         const eventColors = colors.event || {};
56 |         return Object.entries(eventColors)
57 |             .map(([id, colorInfo]) => `Color ID: ${id} - ${colorInfo.background} (background) / ${colorInfo.foreground} (foreground)`)
58 |             .join("\n");
59 |     }
60 | }
61 | 
```

--------------------------------------------------------------------------------
/src/auth/client.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { OAuth2Client } from 'google-auth-library';
 2 | import * as fs from 'fs/promises';
 3 | import { getKeysFilePath, generateCredentialsErrorMessage, OAuthCredentials } from './utils.js';
 4 | 
 5 | async function loadCredentialsFromFile(): Promise<OAuthCredentials> {
 6 |   const keysContent = await fs.readFile(getKeysFilePath(), "utf-8");
 7 |   const keys = JSON.parse(keysContent);
 8 | 
 9 |   if (keys.installed) {
10 |     // Standard OAuth credentials file format
11 |     const { client_id, client_secret, redirect_uris } = keys.installed;
12 |     return { client_id, client_secret, redirect_uris };
13 |   } else if (keys.client_id && keys.client_secret) {
14 |     // Direct format
15 |     return {
16 |       client_id: keys.client_id,
17 |       client_secret: keys.client_secret,
18 |       redirect_uris: keys.redirect_uris || ['http://localhost:3000/oauth2callback']
19 |     };
20 |   } else {
21 |     throw new Error('Invalid credentials file format. Expected either "installed" object or direct client_id/client_secret fields.');
22 |   }
23 | }
24 | 
25 | async function loadCredentialsWithFallback(): Promise<OAuthCredentials> {
26 |   // Load credentials from file (CLI param, env var, or default path)
27 |   try {
28 |     return await loadCredentialsFromFile();
29 |   } catch (fileError) {
30 |     // Generate helpful error message
31 |     const errorMessage = generateCredentialsErrorMessage();
32 |     throw new Error(`${errorMessage}\n\nOriginal error: ${fileError instanceof Error ? fileError.message : fileError}`);
33 |   }
34 | }
35 | 
36 | export async function initializeOAuth2Client(): Promise<OAuth2Client> {
37 |   // Always use real OAuth credentials - no mocking.
38 |   // Unit tests should mock at the handler level, integration tests need real credentials.
39 |   try {
40 |     const credentials = await loadCredentialsWithFallback();
41 |     
42 |     // Use the first redirect URI as the default for the base client
43 |     return new OAuth2Client({
44 |       clientId: credentials.client_id,
45 |       clientSecret: credentials.client_secret,
46 |       redirectUri: credentials.redirect_uris[0],
47 |     });
48 |   } catch (error) {
49 |     throw new Error(`Error loading OAuth keys: ${error instanceof Error ? error.message : error}`);
50 |   }
51 | }
52 | 
53 | export async function loadCredentials(): Promise<{ client_id: string; client_secret: string }> {
54 |   try {
55 |     const credentials = await loadCredentialsWithFallback();
56 |     
57 |     if (!credentials.client_id || !credentials.client_secret) {
58 |         throw new Error('Client ID or Client Secret missing in credentials.');
59 |     }
60 |     return {
61 |       client_id: credentials.client_id,
62 |       client_secret: credentials.client_secret
63 |     };
64 |   } catch (error) {
65 |     throw new Error(`Error loading credentials: ${error instanceof Error ? error.message : error}`);
66 |   }
67 | }
```

--------------------------------------------------------------------------------
/src/tests/unit/handlers/datetime-utils.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, it, expect } from 'vitest';
 2 | import { hasTimezoneInDatetime, convertToRFC3339, createTimeObject } from '../../../handlers/utils/datetime.js';
 3 | 
 4 | describe('Datetime Utilities', () => {
 5 |   describe('hasTimezoneInDatetime', () => {
 6 |     it('should return true for timezone-aware datetime strings', () => {
 7 |       expect(hasTimezoneInDatetime('2024-01-01T10:00:00Z')).toBe(true);
 8 |       expect(hasTimezoneInDatetime('2024-01-01T10:00:00+05:00')).toBe(true);
 9 |       expect(hasTimezoneInDatetime('2024-01-01T10:00:00-08:00')).toBe(true);
10 |     });
11 | 
12 |     it('should return false for timezone-naive datetime strings', () => {
13 |       expect(hasTimezoneInDatetime('2024-01-01T10:00:00')).toBe(false);
14 |       expect(hasTimezoneInDatetime('2024-01-01 10:00:00')).toBe(false);
15 |     });
16 |   });
17 | 
18 |   describe('convertToRFC3339', () => {
19 |     it('should return timezone-aware datetime unchanged', () => {
20 |       const datetime = '2024-01-01T10:00:00Z';
21 |       expect(convertToRFC3339(datetime, 'America/Los_Angeles')).toBe(datetime);
22 |     });
23 | 
24 |     it('should return timezone-aware datetime with offset unchanged', () => {
25 |       const datetime = '2024-01-01T10:00:00-08:00';
26 |       expect(convertToRFC3339(datetime, 'America/Los_Angeles')).toBe(datetime);
27 |     });
28 | 
29 |     it('should convert timezone-naive datetime using fallback timezone', () => {
30 |       const datetime = '2024-06-15T14:30:00';
31 |       const result = convertToRFC3339(datetime, 'UTC');
32 |       
33 |       // Should result in a timezone-aware string (the exact time depends on system timezone)
34 |       expect(result).toMatch(/2024-06-15T\d{2}:\d{2}:\d{2}Z/);
35 |       expect(result).not.toBe(datetime); // Should be different from input
36 |     });
37 | 
38 |     it('should fallback to UTC for invalid timezone conversion', () => {
39 |       const datetime = '2024-01-01T10:00:00';
40 |       const result = convertToRFC3339(datetime, 'Invalid/Timezone');
41 |       
42 |       // Should fallback to UTC
43 |       expect(result).toBe('2024-01-01T10:00:00Z');
44 |     });
45 |   });
46 | 
47 |   describe('createTimeObject', () => {
48 |     it('should create time object without timeZone for timezone-aware datetime', () => {
49 |       const datetime = '2024-01-01T10:00:00Z';
50 |       const result = createTimeObject(datetime, 'America/Los_Angeles');
51 |       
52 |       expect(result).toEqual({
53 |         dateTime: datetime
54 |       });
55 |     });
56 | 
57 |     it('should create time object with timeZone for timezone-naive datetime', () => {
58 |       const datetime = '2024-01-01T10:00:00';
59 |       const timezone = 'America/Los_Angeles';
60 |       const result = createTimeObject(datetime, timezone);
61 |       
62 |       expect(result).toEqual({
63 |         dateTime: datetime,
64 |         timeZone: timezone
65 |       });
66 |     });
67 |   });
68 | });
```

--------------------------------------------------------------------------------
/src/handlers/core/ListCalendarsHandler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
 2 | import { OAuth2Client } from "google-auth-library";
 3 | import { BaseToolHandler } from "./BaseToolHandler.js";
 4 | import { calendar_v3 } from "googleapis";
 5 | import { ListCalendarsResponse } from "../../types/structured-responses.js";
 6 | import { createStructuredResponse } from "../../utils/response-builder.js";
 7 | 
 8 | export class ListCalendarsHandler extends BaseToolHandler {
 9 |     async runTool(_: any, oauth2Client: OAuth2Client): Promise<CallToolResult> {
10 |         const calendars = await this.listCalendars(oauth2Client);
11 | 
12 |         const response: ListCalendarsResponse = {
13 |             calendars: calendars.map(cal => ({
14 |                 id: cal.id || '',
15 |                 summary: cal.summary ?? undefined,
16 |                 description: cal.description ?? undefined,
17 |                 location: cal.location ?? undefined,
18 |                 timeZone: cal.timeZone ?? undefined,
19 |                 summaryOverride: cal.summaryOverride ?? undefined,
20 |                 colorId: cal.colorId ?? undefined,
21 |                 backgroundColor: cal.backgroundColor ?? undefined,
22 |                 foregroundColor: cal.foregroundColor ?? undefined,
23 |                 hidden: cal.hidden ?? undefined,
24 |                 selected: cal.selected ?? undefined,
25 |                 accessRole: cal.accessRole ?? undefined,
26 |                 defaultReminders: cal.defaultReminders?.map(r => ({
27 |                     method: (r.method as 'email' | 'popup') || 'popup',
28 |                     minutes: r.minutes || 0
29 |                 })),
30 |                 notificationSettings: cal.notificationSettings ? {
31 |                     notifications: cal.notificationSettings.notifications?.map(n => ({
32 |                         type: n.type ?? undefined,
33 |                         method: n.method ?? undefined
34 |                     }))
35 |                 } : undefined,
36 |                 primary: cal.primary ?? undefined,
37 |                 deleted: cal.deleted ?? undefined,
38 |                 conferenceProperties: cal.conferenceProperties ? {
39 |                     allowedConferenceSolutionTypes: cal.conferenceProperties.allowedConferenceSolutionTypes ?? undefined
40 |                 } : undefined
41 |             })),
42 |             totalCount: calendars.length
43 |         };
44 | 
45 |         return createStructuredResponse(response);
46 |     }
47 | 
48 |     private async listCalendars(client: OAuth2Client): Promise<calendar_v3.Schema$CalendarListEntry[]> {
49 |         try {
50 |             const calendar = this.getCalendar(client);
51 |             const response = await calendar.calendarList.list();
52 |             return response.data.items || [];
53 |         } catch (error) {
54 |             throw this.handleGoogleApiError(error);
55 |         }
56 |     }
57 | }
58 | 
```

--------------------------------------------------------------------------------
/src/auth-server.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { initializeOAuth2Client } from './auth/client.js';
 2 | import { AuthServer } from './auth/server.js';
 3 | 
 4 | async function runAuthServer() {
 5 |   let authServer: AuthServer | null = null; // Keep reference for cleanup
 6 |   try {
 7 |     const oauth2Client = await initializeOAuth2Client();
 8 |     
 9 |     authServer = new AuthServer(oauth2Client);
10 |     
11 |     const success = await authServer.start(true);
12 |     
13 |     if (!success && !authServer.authCompletedSuccessfully) {
14 |       process.stderr.write('Authentication failed. Could not start server or validate existing tokens.\n');
15 |       process.exit(1);
16 |     } else if (authServer.authCompletedSuccessfully) {
17 |       process.stderr.write('Authentication successful.\n');
18 |       process.exit(0);
19 |     }
20 |     
21 |     // If we reach here, the server started and is waiting for the browser callback
22 |     process.stderr.write('Authentication server started. Please complete the authentication in your browser...\n');
23 |     
24 | 
25 |     process.stderr.write(`Waiting for OAuth callback on port ${authServer.getRunningPort()}...\n`);
26 |     
27 |     // Poll for completion or handle SIGINT
28 |     let lastDebugLog = 0;
29 |     const pollInterval = setInterval(async () => {
30 |       try {
31 |         if (authServer?.authCompletedSuccessfully) {
32 |           process.stderr.write('Authentication completed successfully detected. Stopping server...\n');
33 |           clearInterval(pollInterval);
34 |           await authServer.stop();
35 |           process.stderr.write('Authentication successful. Server stopped.\n');
36 |           process.exit(0);
37 |         } else {
38 |           // Add debug logging every 10 seconds to show we're still waiting
39 |           const now = Date.now();
40 |           if (now - lastDebugLog > 10000) {
41 |             process.stderr.write('Still waiting for authentication to complete...\n');
42 |             lastDebugLog = now;
43 |           }
44 |         }
45 |       } catch (error: unknown) {
46 |         process.stderr.write(`Error in polling interval: ${error instanceof Error ? error.message : 'Unknown error'}\n`);
47 |         clearInterval(pollInterval);
48 |         if (authServer) await authServer.stop();
49 |         process.exit(1);
50 |       }
51 |     }, 5000); // Check every second
52 | 
53 |     // Handle process termination (SIGINT)
54 |     process.on('SIGINT', async () => {
55 |       clearInterval(pollInterval); // Stop polling
56 |       if (authServer) {
57 |         await authServer.stop();
58 |       }
59 |       process.exit(0);
60 |     });
61 |     
62 |   } catch (error: unknown) {
63 |     process.stderr.write(`Authentication error: ${error instanceof Error ? error.message : 'Unknown error'}\n`);
64 |     if (authServer) await authServer.stop(); // Attempt cleanup
65 |     process.exit(1);
66 |   }
67 | }
68 | 
69 | // Run the auth server if this file is executed directly
70 | if (import.meta.url.endsWith('auth-server.js')) {
71 |   runAuthServer().catch((error: unknown) => {
72 |     process.stderr.write(`Unhandled error: ${error instanceof Error ? error.message : 'Unknown error'}\n`);
73 |     process.exit(1);
74 |   });
75 | }
```

--------------------------------------------------------------------------------
/src/tests/unit/handlers/create-event-blocking.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, it, expect, vi } from 'vitest';
 2 | import { CreateEventHandler } from '../../../handlers/core/CreateEventHandler.js';
 3 | import { OAuth2Client } from 'google-auth-library';
 4 | import { calendar_v3 } from 'googleapis';
 5 | import { CONFLICT_DETECTION_CONFIG } from '../../../services/conflict-detection/config.js';
 6 | 
 7 | describe('CreateEventHandler Blocking Logic', () => {
 8 |   const mockOAuth2Client = {
 9 |     getAccessToken: vi.fn().mockResolvedValue({ token: 'mock-token' })
10 |   } as unknown as OAuth2Client;
11 | 
12 |   it('should show full event details when blocking due to high similarity', async () => {
13 |     const handler = new CreateEventHandler();
14 |     
15 |     // Mock the conflict detection service
16 |     const existingEvent: calendar_v3.Schema$Event = {
17 |       id: 'existing-lunch-123',
18 |       summary: 'Lunch with Josh',
19 |       description: 'Monthly catch-up lunch',
20 |       location: 'The Coffee Shop',
21 |       start: { 
22 |         dateTime: '2024-01-15T12:00:00-08:00',
23 |         timeZone: 'America/Los_Angeles'
24 |       },
25 |       end: { 
26 |         dateTime: '2024-01-15T13:00:00-08:00',
27 |         timeZone: 'America/Los_Angeles'
28 |       },
29 |       attendees: [
30 |         { email: '[email protected]', displayName: 'Josh', responseStatus: 'accepted' }
31 |       ],
32 |       htmlLink: 'https://calendar.google.com/calendar/event?eid=existing-lunch-123'
33 |     };
34 | 
35 |     // Mock the checkConflicts method to return a high similarity duplicate
36 |     vi.spyOn(handler['conflictDetectionService'], 'checkConflicts').mockResolvedValue({
37 |       hasConflicts: true,
38 |       duplicates: [{
39 |         event: {
40 |           id: 'existing-lunch-123',
41 |           title: 'Lunch with Josh',
42 |           url: 'https://calendar.google.com/calendar/event?eid=existing-lunch-123',
43 |           similarity: 1.0 // 100% similar
44 |         },
45 |         fullEvent: existingEvent,
46 |         calendarId: 'primary',
47 |         suggestion: 'This appears to be a duplicate. Consider updating the existing event instead.'
48 |       }],
49 |       conflicts: []
50 |     });
51 | 
52 |     // Mock getCalendarTimezone
53 |     vi.spyOn(handler as any, 'getCalendarTimezone').mockResolvedValue('America/Los_Angeles');
54 | 
55 |     const args = {
56 |       calendarId: 'primary',
57 |       summary: 'Lunch with Josh',
58 |       start: '2024-01-15T12:00:00',
59 |       end: '2024-01-15T13:00:00',
60 |       location: 'The Coffee Shop'
61 |     };
62 | 
63 |     // Now it should throw an error instead of returning a text message
64 |     await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow(
65 |       'Duplicate event detected (100% similar). Event "Lunch with Josh" already exists. To create anyway, set allowDuplicates to true.'
66 |     );
67 |   });
68 | 
69 |   it('should use centralized threshold configuration', () => {
70 |     // Verify that the config has the expected thresholds
71 |     expect(CONFLICT_DETECTION_CONFIG.DEFAULT_DUPLICATE_THRESHOLD).toBe(0.7);
72 |     expect(CONFLICT_DETECTION_CONFIG.DUPLICATE_THRESHOLDS.WARNING).toBe(0.7);
73 |     expect(CONFLICT_DETECTION_CONFIG.DUPLICATE_THRESHOLDS.BLOCKING).toBe(0.95);
74 |   });
75 | });
```

--------------------------------------------------------------------------------
/src/tests/unit/handlers/utils.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, it, expect } from 'vitest';
 2 | import { generateEventUrl, getEventUrl } from '../../../handlers/utils.js';
 3 | import { calendar_v3 } from 'googleapis';
 4 | 
 5 | describe('Event URL Utilities', () => {
 6 |     describe('generateEventUrl', () => {
 7 |         it('should generate a proper Google Calendar event URL', () => {
 8 |             const calendarId = '[email protected]';
 9 |             const eventId = 'abc123def456';
10 |             const url = generateEventUrl(calendarId, eventId);
11 |             
12 |             expect(url).toBe('https://calendar.google.com/calendar/event?eid=abc123def456&cid=user%40example.com');
13 |         });
14 | 
15 |         it('should properly encode special characters in calendar ID', () => {
16 |             const calendarId = '[email protected]';
17 |             const eventId = 'event123';
18 |             const url = generateEventUrl(calendarId, eventId);
19 |             
20 |             expect(url).toBe('https://calendar.google.com/calendar/event?eid=event123&cid=user%40test-calendar.com');
21 |         });
22 | 
23 |         it('should properly encode special characters in event ID', () => {
24 |             const calendarId = '[email protected]';
25 |             const eventId = 'event+with+special&chars';
26 |             const url = generateEventUrl(calendarId, eventId);
27 |             
28 |             expect(url).toBe('https://calendar.google.com/calendar/event?eid=event%2Bwith%2Bspecial%26chars&cid=user%40example.com');
29 |         });
30 |     });
31 | 
32 |     describe('getEventUrl', () => {
33 |         const mockEvent: calendar_v3.Schema$Event = {
34 |             id: 'test123',
35 |             summary: 'Test Event',
36 |             start: { dateTime: '2024-03-15T10:00:00-07:00' },
37 |             end: { dateTime: '2024-03-15T11:00:00-07:00' },
38 |             location: 'Conference Room A',
39 |             description: 'Test meeting'
40 |         };
41 | 
42 |         it('should use htmlLink when available', () => {
43 |             const eventWithHtmlLink = {
44 |                 ...mockEvent,
45 |                 htmlLink: 'https://calendar.google.com/event?eid=existing123'
46 |             };
47 |             
48 |             const result = getEventUrl(eventWithHtmlLink);
49 |             expect(result).toBe('https://calendar.google.com/event?eid=existing123');
50 |         });
51 | 
52 |         it('should generate URL when htmlLink is not available but calendarId is provided', () => {
53 |             const result = getEventUrl(mockEvent, '[email protected]');
54 |             expect(result).toBe('https://calendar.google.com/calendar/event?eid=test123&cid=user%40example.com');
55 |         });
56 | 
57 |         it('should return null when htmlLink is not available and calendarId is not provided', () => {
58 |             const result = getEventUrl(mockEvent);
59 |             expect(result).toBeNull();
60 |         });
61 | 
62 |         it('should return null when event has no ID', () => {
63 |             const eventWithoutId = { ...mockEvent, id: undefined };
64 |             const result = getEventUrl(eventWithoutId, '[email protected]');
65 |             expect(result).toBeNull();
66 |         });
67 |     });
68 | });
```

--------------------------------------------------------------------------------
/src/tests/unit/schemas/no-refs.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, it, expect } from 'vitest';
 2 | import { ToolRegistry } from '../../../tools/registry.js';
 3 | 
 4 | describe('Schema $ref Prevention Tests', () => {
 5 |   it('should not generate $ref references in JSON schemas, causes issues with Claude Desktop', () => {
 6 |     const tools = ToolRegistry.getToolsWithSchemas();
 7 |     
 8 |     // Convert each tool schema to JSON Schema and check for $ref
 9 |     for (const tool of tools) {
10 |       const jsonSchema = JSON.stringify(tool.inputSchema);
11 |       
12 |       // Check for any $ref references
13 |       const hasRef = jsonSchema.includes('"$ref"');
14 |       
15 |       if (hasRef) {
16 |         console.error(`Tool "${tool.name}" contains $ref in schema:`, jsonSchema);
17 |       }
18 |       
19 |       expect(hasRef).toBe(false);
20 |     }
21 |   });
22 | 
23 |   it('should have unique schema instances for similar parameters', () => {
24 |     const tools = ToolRegistry.getToolsWithSchemas();
25 |     
26 |     // Find tools with timeMin/timeMax or start/end parameters
27 |     const timeParams = [];
28 |     
29 |     for (const tool of tools) {
30 |       const schemaStr = JSON.stringify(tool.inputSchema);
31 |       if (schemaStr.includes('timeMin') || schemaStr.includes('timeMax') || 
32 |           schemaStr.includes('"start"') || schemaStr.includes('"end"')) {
33 |         timeParams.push(tool.name);
34 |       }
35 |     }
36 |     
37 |     // Ensure we're testing the right tools
38 |     expect(timeParams.length).toBeGreaterThan(0);
39 |     console.log('Tools with time parameters:', timeParams);
40 |   });
41 | 
42 |   it('should detect if shared schema instances are reused', () => {
43 |     // This test checks the source code structure to prevent regression
44 |     const registryCode = require('fs').readFileSync(
45 |       require('path').join(__dirname, '../../../tools/registry.ts'), 
46 |       'utf8'
47 |     );
48 |     
49 |     // Check for problematic patterns that could cause $ref generation
50 |     // Note: Removed negative lookahead (?!\.) to catch ALL schema reuse including .optional() and .describe()
51 |     const sharedSchemaUsage = [
52 |       /timeMin:\s*[A-Z][a-zA-Z]*Schema/,     // timeMin: SomeSchema (any usage)
53 |       /timeMax:\s*[A-Z][a-zA-Z]*Schema/,     // timeMax: SomeSchema
54 |       /start:\s*[A-Z][a-zA-Z]*Schema/,       // start: SomeSchema
55 |       /end:\s*[A-Z][a-zA-Z]*Schema/,         // end: SomeSchema
56 |       /calendarId:\s*[A-Z][a-zA-Z]*Schema/,  // calendarId: SomeSchema
57 |       /eventId:\s*[A-Z][a-zA-Z]*Schema/,     // eventId: SomeSchema
58 |       /query:\s*[A-Z][a-zA-Z]*Schema/,       // query: SomeSchema
59 |       /summary:\s*[A-Z][a-zA-Z]*Schema/,     // summary: SomeSchema
60 |       /description:\s*[A-Z][a-zA-Z]*Schema/, // description: SomeSchema
61 |       /location:\s*[A-Z][a-zA-Z]*Schema/,    // location: SomeSchema
62 |       /colorId:\s*[A-Z][a-zA-Z]*Schema/,     // colorId: SomeSchema
63 |       /reminders:\s*[A-Z][a-zA-Z]*Schema/,   // reminders: SomeSchema
64 |       /attendees:\s*[A-Z][a-zA-Z]*Schema/,   // attendees: SomeSchema
65 |       /email:\s*[A-Z][a-zA-Z]*Schema/        // email: SomeSchema
66 |     ];
67 |     
68 |     for (const pattern of sharedSchemaUsage) {
69 |       const matches = registryCode.match(pattern);
70 |       if (matches) {
71 |         console.error(`Found potentially problematic schema usage: ${matches[0]}`);
72 |         expect(matches).toBeNull();
73 |       }
74 |     }
75 |   });
76 | });
```

--------------------------------------------------------------------------------
/src/utils/event-id-validator.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Event ID validation utility for Google Calendar API
  3 |  */
  4 | 
  5 | /**
  6 |  * Validates a custom event ID according to Google Calendar requirements
  7 |  * @param eventId The event ID to validate
  8 |  * @returns true if valid, false otherwise
  9 |  */
 10 | export function isValidEventId(eventId: string): boolean {
 11 |   // Check length constraints (5-1024 characters)
 12 |   if (eventId.length < 5 || eventId.length > 1024) {
 13 |     return false;
 14 |   }
 15 |   
 16 |   // Check character constraints (base32hex encoding)
 17 |   // Google Calendar allows only: lowercase letters a-v and digits 0-9
 18 |   // Based on RFC2938 section 3.1.2
 19 |   const validPattern = /^[a-v0-9]+$/;
 20 |   return validPattern.test(eventId);
 21 | }
 22 | 
 23 | /**
 24 |  * Validates and throws an error if the event ID is invalid
 25 |  * @param eventId The event ID to validate
 26 |  * @throws Error if the event ID is invalid
 27 |  */
 28 | export function validateEventId(eventId: string): void {
 29 |   if (!isValidEventId(eventId)) {
 30 |     const errors: string[] = [];
 31 |     
 32 |     if (eventId.length < 5) {
 33 |       errors.push("must be at least 5 characters long");
 34 |     }
 35 |     
 36 |     if (eventId.length > 1024) {
 37 |       errors.push("must not exceed 1024 characters");
 38 |     }
 39 |     
 40 |     if (!/^[a-v0-9]+$/.test(eventId)) {
 41 |       errors.push("can only contain lowercase letters a-v and digits 0-9 (base32hex encoding)");
 42 |     }
 43 |     
 44 |     throw new Error(`Invalid event ID: ${errors.join(", ")}`);
 45 |   }
 46 | }
 47 | 
 48 | /**
 49 |  * Sanitizes a string to make it a valid event ID
 50 |  * Converts to base32hex encoding (lowercase a-v and 0-9 only)
 51 |  * @param input The input string to sanitize
 52 |  * @returns A valid event ID
 53 |  */
 54 | export function sanitizeEventId(input: string): string {
 55 |   // Convert to lowercase first
 56 |   let sanitized = input.toLowerCase();
 57 |   
 58 |   // Replace invalid characters:
 59 |   // - Keep digits 0-9 as is
 60 |   // - Map letters w-z to a-d (shift back)
 61 |   // - Map other characters to valid base32hex characters
 62 |   sanitized = sanitized.replace(/[^a-v0-9]/g, (char) => {
 63 |     // Map w-z to a-d
 64 |     if (char >= 'w' && char <= 'z') {
 65 |       return String.fromCharCode(char.charCodeAt(0) - 22); // w->a, x->b, y->c, z->d
 66 |     }
 67 |     // Map any other character to a default valid character
 68 |     return '';
 69 |   });
 70 |   
 71 |   // Remove any empty spaces from the mapping
 72 |   sanitized = sanitized.replace(/\s+/g, '');
 73 |   
 74 |   // Ensure minimum length
 75 |   if (sanitized.length < 5) {
 76 |     // Generate a base32hex timestamp
 77 |     const timestamp = Date.now().toString(32).replace(/[w-z]/g, (c) => 
 78 |       String.fromCharCode(c.charCodeAt(0) - 22)
 79 |     );
 80 |     
 81 |     if (sanitized.length === 0) {
 82 |       sanitized = `event${timestamp}`.substring(0, 26); // Match Google's 26-char format
 83 |     } else {
 84 |       sanitized = `${sanitized}${timestamp}`.substring(0, 26);
 85 |     }
 86 |   }
 87 |   
 88 |   // Ensure maximum length
 89 |   if (sanitized.length > 1024) {
 90 |     sanitized = sanitized.slice(0, 1024);
 91 |   }
 92 |   
 93 |   // Final validation - ensure only valid characters
 94 |   sanitized = sanitized.replace(/[^a-v0-9]/g, '');
 95 |   
 96 |   // If still too short after all operations, generate a default
 97 |   if (sanitized.length < 5) {
 98 |     // Generate a valid base32hex ID
 99 |     const now = Date.now();
100 |     const base32hex = now.toString(32).replace(/[w-z]/g, (c) => 
101 |       String.fromCharCode(c.charCodeAt(0) - 22)
102 |     );
103 |     sanitized = `ev${base32hex}`.substring(0, 26);
104 |   }
105 |   
106 |   return sanitized;
107 | }
```

--------------------------------------------------------------------------------
/src/handlers/core/SearchEventsHandler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
 2 | import { OAuth2Client } from "google-auth-library";
 3 | import { SearchEventsInput } from "../../tools/registry.js";
 4 | import { BaseToolHandler } from "./BaseToolHandler.js";
 5 | import { calendar_v3 } from 'googleapis';
 6 | import { convertToRFC3339 } from "../utils/datetime.js";
 7 | import { buildListFieldMask } from "../../utils/field-mask-builder.js";
 8 | import { createStructuredResponse, convertEventsToStructured } from "../../utils/response-builder.js";
 9 | import { SearchEventsResponse } from "../../types/structured-responses.js";
10 | 
11 | export class SearchEventsHandler extends BaseToolHandler {
12 |     async runTool(args: any, oauth2Client: OAuth2Client): Promise<CallToolResult> {
13 |         const validArgs = args as SearchEventsInput;
14 |         const events = await this.searchEvents(oauth2Client, validArgs);
15 |         
16 |         const response: SearchEventsResponse = {
17 |             events: convertEventsToStructured(events, validArgs.calendarId),
18 |             totalCount: events.length,
19 |             query: validArgs.query,
20 |             calendarId: validArgs.calendarId
21 |         };
22 |         
23 |         if (validArgs.timeMin || validArgs.timeMax) {
24 |             const timezone = validArgs.timeZone || await this.getCalendarTimezone(oauth2Client, validArgs.calendarId);
25 |             response.timeRange = {
26 |                 start: validArgs.timeMin ? convertToRFC3339(validArgs.timeMin, timezone) : '',
27 |                 end: validArgs.timeMax ? convertToRFC3339(validArgs.timeMax, timezone) : ''
28 |             };
29 |         }
30 |         
31 |         return createStructuredResponse(response);
32 |     }
33 | 
34 |     private async searchEvents(
35 |         client: OAuth2Client,
36 |         args: SearchEventsInput
37 |     ): Promise<calendar_v3.Schema$Event[]> {
38 |         try {
39 |             const calendar = this.getCalendar(client);
40 |             
41 |             // Determine timezone with correct precedence:
42 |             // 1. Explicit timeZone parameter (highest priority)
43 |             // 2. Calendar's default timezone (fallback)
44 |             const timezone = args.timeZone || await this.getCalendarTimezone(client, args.calendarId);
45 |             
46 |             // Convert time boundaries to RFC3339 format for Google Calendar API
47 |             // Note: convertToRFC3339 will still respect timezone in datetime string as highest priority
48 |             const timeMin = convertToRFC3339(args.timeMin, timezone);
49 |             const timeMax = convertToRFC3339(args.timeMax, timezone);
50 |             
51 |             const fieldMask = buildListFieldMask(args.fields);
52 |             
53 |             const response = await calendar.events.list({
54 |                 calendarId: args.calendarId,
55 |                 q: args.query,
56 |                 timeMin,
57 |                 timeMax,
58 |                 singleEvents: true,
59 |                 orderBy: 'startTime',
60 |                 ...(fieldMask && { fields: fieldMask }),
61 |                 ...(args.privateExtendedProperty && { privateExtendedProperty: args.privateExtendedProperty as any }),
62 |                 ...(args.sharedExtendedProperty && { sharedExtendedProperty: args.sharedExtendedProperty as any })
63 |             });
64 |             return response.data.items || [];
65 |         } catch (error) {
66 |             throw this.handleGoogleApiError(error);
67 |         }
68 |     }
69 | 
70 | }
71 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Changelog
 2 | 
 3 | ## [2.0.6](https://github.com/nspady/google-calendar-mcp/compare/v2.0.5...v2.0.6) (2025-10-22)
 4 | 
 5 | 
 6 | ### Bug Fixes
 7 | 
 8 | * support converting between timed and all-day events in update-event ([#119](https://github.com/nspady/google-calendar-mcp/issues/119)) ([407e4c8](https://github.com/nspady/google-calendar-mcp/commit/407e4c89753932e13f9ccd55800999b4b12288be))
 9 | 
10 | ## [2.0.5](https://github.com/nspady/google-calendar-mcp/compare/v2.0.4...v2.0.5) (2025-10-19)
11 | 
12 | 
13 | ### Bug Fixes
14 | 
15 | * **list-events:** support native arrays for Python MCP clients ([#95](https://github.com/nspady/google-calendar-mcp/issues/95)) ([#116](https://github.com/nspady/google-calendar-mcp/issues/116)) ([0e91c23](https://github.com/nspady/google-calendar-mcp/commit/0e91c23c9ae9db0c0ff863cd9019f6212544f62a))
16 | 
17 | ## [2.0.4](https://github.com/nspady/google-calendar-mcp/compare/v2.0.3...v2.0.4) (2025-10-15)
18 | 
19 | 
20 | ### Bug Fixes
21 | 
22 | * resolve macOS installation error and improve publish workflow ([ec13f39](https://github.com/nspady/google-calendar-mcp/commit/ec13f397652a864cccd003f05ddd03d4e046316f)), closes [#113](https://github.com/nspady/google-calendar-mcp/issues/113)
23 | 
24 | ## [2.0.3](https://github.com/nspady/google-calendar-mcp/compare/v2.0.2...v2.0.3) (2025-10-15)
25 | 
26 | 
27 | ### Bug Fixes
28 | 
29 | * move esbuild to devDependencies and fix publish workflow ([3900358](https://github.com/nspady/google-calendar-mcp/commit/39003589278dbab95c85f27af012293405f34f74)), closes [#113](https://github.com/nspady/google-calendar-mcp/issues/113)
30 | 
31 | ## [2.0.2](https://github.com/nspady/google-calendar-mcp/compare/v2.0.1...v2.0.2) (2025-10-14)
32 | 
33 | 
34 | ### Bug Fixes
35 | 
36 | * **auth:** improve port availability error message ([9205fd7](https://github.com/nspady/google-calendar-mcp/commit/9205fd75445702d9e49520e4183c96a93078ea46)), closes [#110](https://github.com/nspady/google-calendar-mcp/issues/110)
37 | 
38 | ## [2.0.1](https://github.com/nspady/google-calendar-mcp/compare/v2.0.0...v2.0.1) (2025-10-13)
39 | 
40 | 
41 | ### Bug Fixes
42 | 
43 | * auto-resolve calendar names and summaryOverride to IDs (closes [#104](https://github.com/nspady/google-calendar-mcp/issues/104)) ([#105](https://github.com/nspady/google-calendar-mcp/issues/105)) ([d10225c](https://github.com/nspady/google-calendar-mcp/commit/d10225ca767a0641fef118cf3d56869bf66e2421))
44 | * Resolve rollup optional dependency issue in CI ([#102](https://github.com/nspady/google-calendar-mcp/issues/102)) ([0bc39bd](https://github.com/nspady/google-calendar-mcp/commit/0bc39bd54fdb57828b033153974e1a93e2b38737))
45 | * Support single-quoted JSON arrays in list-events calendarId ([d2af7cf](https://github.com/nspady/google-calendar-mcp/commit/d2af7cf99e3d090bceb388cbf10f7f9649100e3c))
46 | * update publish workflow to use release-please ([47addc9](https://github.com/nspady/google-calendar-mcp/commit/47addc95cc04e552017afd7523638795bf9f9090))
47 | 
48 | ## [2.0.2](https://github.com/nspady/google-calendar-mcp/compare/v2.0.1...v2.0.2) (2025-10-13)
49 | 
50 | ### Bug Fixes
51 | 
52 | * auto-resolve calendar names and summaryOverride to IDs (closes [#104](https://github.com/nspady/google-calendar-mcp/issues/104)) ([#105](https://github.com/nspady/google-calendar-mcp/issues/105)) ([d10225c](https://github.com/nspady/google-calendar-mcp/commit/d10225ca767a0641fef118cf3d56869bf66e2421))
53 | * update publish workflow to use release-please ([47addc9](https://github.com/nspady/google-calendar-mcp/commit/47addc95cc04e552017afd7523638795bf9f9090))
54 | 
55 | ## [2.0.1](https://github.com/nspady/google-calendar-mcp/compare/v2.0.0...v2.0.1) (2025-10-11)
56 | 
57 | ### Bug Fixes
58 | 
59 | * Resolve rollup optional dependency issue in CI ([#102](https://github.com/nspady/google-calendar-mcp/issues/102)) ([0bc39bd](https://github.com/nspady/google-calendar-mcp/commit/0bc39bd54fdb57828b033153974e1a93e2b38737))
60 | * Support single-quoted JSON arrays in list-events calendarId ([d2af7cf](https://github.com/nspady/google-calendar-mcp/commit/d2af7cf99e3d090bceb388cbf10f7f9649100e3c))
61 | 
```

--------------------------------------------------------------------------------
/src/utils/field-mask-builder.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Field mask builder for Google Calendar API partial response
  3 |  */
  4 | 
  5 | // Allowed fields that can be requested from Google Calendar API
  6 | export const ALLOWED_EVENT_FIELDS = [
  7 |   'id',
  8 |   'summary',
  9 |   'description',
 10 |   'start',
 11 |   'end',
 12 |   'location',
 13 |   'attendees',
 14 |   'colorId',
 15 |   'transparency',
 16 |   'extendedProperties',
 17 |   'reminders',
 18 |   'conferenceData',
 19 |   'attachments',
 20 |   'status',
 21 |   'htmlLink',
 22 |   'created',
 23 |   'updated',
 24 |   'creator',
 25 |   'organizer',
 26 |   'recurrence',
 27 |   'recurringEventId',
 28 |   'originalStartTime',
 29 |   'visibility',
 30 |   'iCalUID',
 31 |   'sequence',
 32 |   'hangoutLink',
 33 |   'anyoneCanAddSelf',
 34 |   'guestsCanInviteOthers',
 35 |   'guestsCanModify',
 36 |   'guestsCanSeeOtherGuests',
 37 |   'privateCopy',
 38 |   'locked',
 39 |   'source',
 40 |   'eventType'
 41 | ] as const;
 42 | 
 43 | export type AllowedEventField = typeof ALLOWED_EVENT_FIELDS[number];
 44 | 
 45 | // Default fields always included
 46 | export const DEFAULT_EVENT_FIELDS: AllowedEventField[] = [
 47 |   'id',
 48 |   'summary',
 49 |   'start',
 50 |   'end',
 51 |   'status',
 52 |   'htmlLink',
 53 |   'location',
 54 |   'attendees'
 55 | ];
 56 | 
 57 | /**
 58 |  * Validates that requested fields are allowed
 59 |  */
 60 | export function validateFields(fields: string[]): AllowedEventField[] {
 61 |   const validFields: AllowedEventField[] = [];
 62 |   const invalidFields: string[] = [];
 63 |   
 64 |   for (const field of fields) {
 65 |     if (ALLOWED_EVENT_FIELDS.includes(field as AllowedEventField)) {
 66 |       validFields.push(field as AllowedEventField);
 67 |     } else {
 68 |       invalidFields.push(field);
 69 |     }
 70 |   }
 71 |   
 72 |   if (invalidFields.length > 0) {
 73 |     throw new Error(`Invalid fields requested: ${invalidFields.join(', ')}. Allowed fields: ${ALLOWED_EVENT_FIELDS.join(', ')}`);
 74 |   }
 75 |   
 76 |   return validFields;
 77 | }
 78 | 
 79 | /**
 80 |  * Builds a Google Calendar API field mask for partial response
 81 |  * @param requestedFields Optional array of additional fields to include
 82 |  * @param includeDefaults Whether to include default fields (default: true)
 83 |  * @returns Field mask string for Google Calendar API
 84 |  */
 85 | export function buildEventFieldMask(
 86 |   requestedFields?: string[], 
 87 |   includeDefaults: boolean = true
 88 | ): string | undefined {
 89 |   // If no custom fields requested and we should include defaults, return undefined
 90 |   // to let Google API return its default field set
 91 |   if (!requestedFields || requestedFields.length === 0) {
 92 |     return undefined;
 93 |   }
 94 |   
 95 |   // Validate requested fields
 96 |   const validFields = validateFields(requestedFields);
 97 |   
 98 |   // Combine with defaults if needed
 99 |   const allFields = includeDefaults 
100 |     ? [...new Set([...DEFAULT_EVENT_FIELDS, ...validFields])]
101 |     : validFields;
102 |   
103 |   // Build the field mask for events.list
104 |   // Format: items(field1,field2,field3)
105 |   return `items(${allFields.join(',')})`;
106 | }
107 | 
108 | /**
109 |  * Builds a field mask for a single event (events.get)
110 |  */
111 | export function buildSingleEventFieldMask(
112 |   requestedFields?: string[],
113 |   includeDefaults: boolean = true
114 | ): string | undefined {
115 |   // If no custom fields requested, return undefined for default response
116 |   if (!requestedFields || requestedFields.length === 0) {
117 |     return undefined;
118 |   }
119 |   
120 |   // Validate requested fields
121 |   const validFields = validateFields(requestedFields);
122 |   
123 |   // Combine with defaults if needed
124 |   const allFields = includeDefaults 
125 |     ? [...new Set([...DEFAULT_EVENT_FIELDS, ...validFields])]
126 |     : validFields;
127 |   
128 |   // For single event, just return comma-separated fields
129 |   return allFields.join(',');
130 | }
131 | 
132 | /**
133 |  * Builds the full field mask parameter for list operations
134 |  * Includes nextPageToken, nextSyncToken, etc.
135 |  */
136 | export function buildListFieldMask(
137 |   requestedFields?: string[],
138 |   includeDefaults: boolean = true
139 | ): string | undefined {
140 |   // If no custom fields requested, return undefined for default response
141 |   if (!requestedFields || requestedFields.length === 0) {
142 |     return undefined;
143 |   }
144 |   
145 |   const eventFieldMask = buildEventFieldMask(requestedFields, includeDefaults);
146 |   if (!eventFieldMask) {
147 |     return undefined;
148 |   }
149 |   
150 |   // Include pagination tokens and other list metadata
151 |   return `${eventFieldMask},nextPageToken,nextSyncToken,kind,etag,summary,updated,timeZone,accessRole,defaultReminders`;
152 | }
```

--------------------------------------------------------------------------------
/src/transports/http.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
  3 | import http from "http";
  4 | 
  5 | export interface HttpTransportConfig {
  6 |   port?: number;
  7 |   host?: string;
  8 | }
  9 | 
 10 | export class HttpTransportHandler {
 11 |   private server: McpServer;
 12 |   private config: HttpTransportConfig;
 13 | 
 14 |   constructor(server: McpServer, config: HttpTransportConfig = {}) {
 15 |     this.server = server;
 16 |     this.config = config;
 17 |   }
 18 | 
 19 |   async connect(): Promise<void> {
 20 |     const port = this.config.port || 3000;
 21 |     const host = this.config.host || '127.0.0.1';
 22 | 
 23 |     // Configure transport for stateless mode to allow multiple initialization cycles
 24 |     const transport = new StreamableHTTPServerTransport({
 25 |       sessionIdGenerator: undefined // Stateless mode - allows multiple initializations
 26 |     });
 27 | 
 28 |     await this.server.connect(transport);
 29 | 
 30 |     // Create HTTP server to handle the StreamableHTTP transport
 31 |     const httpServer = http.createServer(async (req, res) => {
 32 |       // Validate Origin header to prevent DNS rebinding attacks (MCP spec requirement)
 33 |       const origin = req.headers.origin;
 34 |       const allowedOrigins = [
 35 |         'http://localhost',
 36 |         'http://127.0.0.1',
 37 |         'https://localhost',
 38 |         'https://127.0.0.1'
 39 |       ];
 40 | 
 41 |       // For requests with Origin header, validate it
 42 |       if (origin && !allowedOrigins.some(allowed => origin.startsWith(allowed))) {
 43 |         res.writeHead(403, { 'Content-Type': 'application/json' });
 44 |         res.end(JSON.stringify({
 45 |           error: 'Forbidden: Invalid origin',
 46 |           message: 'Origin header validation failed'
 47 |         }));
 48 |         return;
 49 |       }
 50 | 
 51 |       // Basic request size limiting (prevent DoS)
 52 |       const contentLength = parseInt(req.headers['content-length'] || '0', 10);
 53 |       const maxRequestSize = 10 * 1024 * 1024; // 10MB limit
 54 |       if (contentLength > maxRequestSize) {
 55 |         res.writeHead(413, { 'Content-Type': 'application/json' });
 56 |         res.end(JSON.stringify({
 57 |           error: 'Payload Too Large',
 58 |           message: 'Request size exceeds maximum allowed size'
 59 |         }));
 60 |         return;
 61 |       }
 62 | 
 63 |       // Handle CORS
 64 |       res.setHeader('Access-Control-Allow-Origin', '*');
 65 |       res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
 66 |       res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id');
 67 |       
 68 |       if (req.method === 'OPTIONS') {
 69 |         res.writeHead(200);
 70 |         res.end();
 71 |         return;
 72 |       }
 73 | 
 74 |       // Validate Accept header for MCP requests (spec requirement)
 75 |       if (req.method === 'POST' || req.method === 'GET') {
 76 |         const acceptHeader = req.headers.accept;
 77 |         if (acceptHeader && !acceptHeader.includes('application/json') && !acceptHeader.includes('text/event-stream') && !acceptHeader.includes('*/*')) {
 78 |           res.writeHead(406, { 'Content-Type': 'application/json' });
 79 |           res.end(JSON.stringify({
 80 |             error: 'Not Acceptable',
 81 |             message: 'Accept header must include application/json or text/event-stream'
 82 |           }));
 83 |           return;
 84 |         }
 85 |       }
 86 | 
 87 |       // Handle health check endpoint
 88 |       if (req.method === 'GET' && req.url === '/health') {
 89 |         res.writeHead(200, { 'Content-Type': 'application/json' });
 90 |         res.end(JSON.stringify({
 91 |           status: 'healthy',
 92 |           server: 'google-calendar-mcp',
 93 |           timestamp: new Date().toISOString()
 94 |         }));
 95 |         return;
 96 |       }
 97 | 
 98 |       try {
 99 |         await transport.handleRequest(req, res);
100 |       } catch (error) {
101 |         process.stderr.write(`Error handling request: ${error instanceof Error ? error.message : error}\n`);
102 |         if (!res.headersSent) {
103 |           res.writeHead(500, { 'Content-Type': 'application/json' });
104 |           res.end(JSON.stringify({
105 |             jsonrpc: '2.0',
106 |             error: {
107 |               code: -32603,
108 |               message: 'Internal server error',
109 |             },
110 |             id: null,
111 |           }));
112 |         }
113 |       }
114 |     });
115 | 
116 |     httpServer.listen(port, host, () => {
117 |       process.stderr.write(`Google Calendar MCP Server listening on http://${host}:${port}\n`);
118 |     });
119 |   }
120 | } 
```

--------------------------------------------------------------------------------
/src/services/conflict-detection/ConflictAnalyzer.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { calendar_v3 } from "googleapis";
  2 | import { EventTimeRange } from "./types.js";
  3 | import { EventSimilarityChecker } from "./EventSimilarityChecker.js";
  4 | 
  5 | export class ConflictAnalyzer {
  6 |   private similarityChecker: EventSimilarityChecker;
  7 |   
  8 |   constructor() {
  9 |     this.similarityChecker = new EventSimilarityChecker();
 10 |   }
 11 |   /**
 12 |    * Analyze overlap between two events
 13 |    * Uses consolidated overlap logic from EventSimilarityChecker
 14 |    */
 15 |   analyzeOverlap(event1: calendar_v3.Schema$Event, event2: calendar_v3.Schema$Event): {
 16 |     hasOverlap: boolean;
 17 |     duration?: string;
 18 |     percentage?: number;
 19 |     startTime?: string;
 20 |     endTime?: string;
 21 |   } {
 22 |     // Use consolidated overlap check
 23 |     const hasOverlap = this.similarityChecker.eventsOverlap(event1, event2);
 24 |     
 25 |     if (!hasOverlap) {
 26 |       return { hasOverlap: false };
 27 |     }
 28 |     
 29 |     // Get time ranges for detailed analysis
 30 |     const time1 = this.getEventTimeRange(event1);
 31 |     const time2 = this.getEventTimeRange(event2);
 32 |     
 33 |     if (!time1 || !time2) {
 34 |       return { hasOverlap: false };
 35 |     }
 36 |     
 37 |     // Calculate overlap details
 38 |     const overlapDuration = this.similarityChecker.calculateOverlapDuration(event1, event2);
 39 |     const overlapStart = new Date(Math.max(time1.start.getTime(), time2.start.getTime()));
 40 |     const overlapEnd = new Date(Math.min(time1.end.getTime(), time2.end.getTime()));
 41 |     
 42 |     // Calculate percentage of overlap relative to the first event
 43 |     const event1Duration = time1.end.getTime() - time1.start.getTime();
 44 |     const overlapPercentage = Math.round((overlapDuration / event1Duration) * 100);
 45 |     
 46 |     return {
 47 |       hasOverlap: true,
 48 |       duration: this.formatDuration(overlapDuration),
 49 |       percentage: overlapPercentage,
 50 |       startTime: overlapStart.toISOString(),
 51 |       endTime: overlapEnd.toISOString()
 52 |     };
 53 |   }
 54 | 
 55 |   /**
 56 |    * Get event time range
 57 |    */
 58 |   private getEventTimeRange(event: calendar_v3.Schema$Event): EventTimeRange | null {
 59 |     const startTime = event.start?.dateTime || event.start?.date;
 60 |     const endTime = event.end?.dateTime || event.end?.date;
 61 |     
 62 |     if (!startTime || !endTime) return null;
 63 |     
 64 |     const start = new Date(startTime);
 65 |     const end = new Date(endTime);
 66 |     
 67 |     // Check if it's an all-day event
 68 |     const isAllDay = !event.start?.dateTime && !!event.start?.date;
 69 |     
 70 |     return { start, end, isAllDay };
 71 |   }
 72 | 
 73 |   /**
 74 |    * Format duration in human-readable format
 75 |    */
 76 |   private formatDuration(milliseconds: number): string {
 77 |     const minutes = Math.floor(milliseconds / (1000 * 60));
 78 |     const hours = Math.floor(minutes / 60);
 79 |     const days = Math.floor(hours / 24);
 80 |     
 81 |     if (days > 0) {
 82 |       const remainingHours = hours % 24;
 83 |       return remainingHours > 0 
 84 |         ? `${days} day${days > 1 ? 's' : ''} ${remainingHours} hour${remainingHours > 1 ? 's' : ''}`
 85 |         : `${days} day${days > 1 ? 's' : ''}`;
 86 |     }
 87 |     
 88 |     if (hours > 0) {
 89 |       const remainingMinutes = minutes % 60;
 90 |       return remainingMinutes > 0
 91 |         ? `${hours} hour${hours > 1 ? 's' : ''} ${remainingMinutes} minute${remainingMinutes > 1 ? 's' : ''}`
 92 |         : `${hours} hour${hours > 1 ? 's' : ''}`;
 93 |     }
 94 |     
 95 |     return `${minutes} minute${minutes > 1 ? 's' : ''}`;
 96 |   }
 97 | 
 98 |   /**
 99 |    * Check if an event conflicts with a busy time slot
100 |    */
101 |   checkBusyConflict(event: calendar_v3.Schema$Event, busySlot: { start?: string | null; end?: string | null }): boolean {
102 |     // Handle null values from Google's API
103 |     const start = busySlot.start ?? undefined;
104 |     const end = busySlot.end ?? undefined;
105 |     
106 |     if (!start || !end) return false;
107 |     
108 |     // Convert busy slot to event format for consistency
109 |     const busyEvent: calendar_v3.Schema$Event = {
110 |       start: { dateTime: start },
111 |       end: { dateTime: end }
112 |     };
113 |     
114 |     return this.similarityChecker.eventsOverlap(event, busyEvent);
115 |   }
116 | 
117 |   /**
118 |    * Filter events that overlap with a given time range
119 |    */
120 |   findOverlappingEvents(
121 |     events: calendar_v3.Schema$Event[],
122 |     targetEvent: calendar_v3.Schema$Event
123 |   ): calendar_v3.Schema$Event[] {
124 |     return events.filter(event => {
125 |       // Skip the same event
126 |       if (event.id === targetEvent.id) return false;
127 |       
128 |       // Skip cancelled events
129 |       if (event.status === 'cancelled') return false;
130 |       
131 |       // Use consolidated overlap check
132 |       return this.similarityChecker.eventsOverlap(targetEvent, event);
133 |     });
134 |   }
135 | }
```

--------------------------------------------------------------------------------
/src/auth/utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as path from 'path';
  2 | import * as os from 'os';
  3 | import * as fs from 'fs';
  4 | import { fileURLToPath } from 'url';
  5 | import { getSecureTokenPath as getSharedSecureTokenPath, getLegacyTokenPath as getSharedLegacyTokenPath, getAccountMode as getSharedAccountMode } from './paths.js';
  6 | 
  7 | // Helper to get the project root directory reliably
  8 | function getProjectRoot(): string {
  9 |   const __dirname = path.dirname(fileURLToPath(import.meta.url));
 10 |   // In build output (e.g., build/bundle.js), __dirname is .../build
 11 |   // Go up ONE level to get the project root
 12 |   const projectRoot = path.join(__dirname, ".."); // Corrected: Go up ONE level
 13 |   return path.resolve(projectRoot); // Ensure absolute path
 14 | }
 15 | 
 16 | // Get the current account mode (normal or test) - delegates to shared implementation
 17 | export function getAccountMode(): 'normal' | 'test' {
 18 |   return getSharedAccountMode() as 'normal' | 'test';
 19 | }
 20 | 
 21 | // Helper to detect if we're running in a test environment
 22 | function isRunningInTestEnvironment(): boolean {
 23 |   // Simple and reliable: just check NODE_ENV
 24 |   return process.env.NODE_ENV === 'test';
 25 | }
 26 | 
 27 | // Returns the absolute path for the saved token file - delegates to shared implementation
 28 | export function getSecureTokenPath(): string {
 29 |   return getSharedSecureTokenPath();
 30 | }
 31 | 
 32 | // Returns the legacy token path for backward compatibility - delegates to shared implementation  
 33 | export function getLegacyTokenPath(): string {
 34 |   return getSharedLegacyTokenPath();
 35 | }
 36 | 
 37 | // Returns the absolute path for the GCP OAuth keys file with priority:
 38 | // 1. Environment variable GOOGLE_OAUTH_CREDENTIALS (highest priority)
 39 | // 2. Default file path (lowest priority)
 40 | export function getKeysFilePath(): string {
 41 |   // Priority 1: Environment variable
 42 |   const envCredentialsPath = process.env.GOOGLE_OAUTH_CREDENTIALS;
 43 |   if (envCredentialsPath) {
 44 |     return path.resolve(envCredentialsPath);
 45 |   }
 46 |   
 47 |   // Priority 2: Default file path
 48 |   const projectRoot = getProjectRoot();
 49 |   const keysPath = path.join(projectRoot, "gcp-oauth.keys.json");
 50 |   return keysPath; // Already absolute from getProjectRoot
 51 | }
 52 | 
 53 | // Helper to determine if we're currently in test mode
 54 | export function isTestMode(): boolean {
 55 |   return getAccountMode() === 'test';
 56 | }
 57 | 
 58 | // Interface for OAuth credentials
 59 | export interface OAuthCredentials {
 60 |   client_id: string;
 61 |   client_secret: string;
 62 |   redirect_uris: string[];
 63 | }
 64 | 
 65 | // Interface for credentials file with project_id
 66 | export interface OAuthCredentialsWithProject {
 67 |   installed?: {
 68 |     project_id?: string;
 69 |     client_id?: string;
 70 |     client_secret?: string;
 71 |     redirect_uris?: string[];
 72 |   };
 73 |   project_id?: string;
 74 |   client_id?: string;
 75 |   client_secret?: string;
 76 |   redirect_uris?: string[];
 77 | }
 78 | 
 79 | // Get project ID from OAuth credentials file
 80 | // Returns undefined if credentials file doesn't exist, is invalid, or missing project_id
 81 | export function getCredentialsProjectId(): string | undefined {
 82 |   try {
 83 |     // Use existing helper to get credentials file path
 84 |     const credentialsPath = getKeysFilePath();
 85 | 
 86 |     if (!fs.existsSync(credentialsPath)) {
 87 |       return undefined;
 88 |     }
 89 | 
 90 |     const credentialsContent = fs.readFileSync(credentialsPath, 'utf-8');
 91 |     const credentials: OAuthCredentialsWithProject = JSON.parse(credentialsContent);
 92 | 
 93 |     // Extract project_id from installed format or direct format
 94 |     if (credentials.installed?.project_id) {
 95 |       return credentials.installed.project_id;
 96 |     } else if (credentials.project_id) {
 97 |       return credentials.project_id;
 98 |     }
 99 | 
100 |     return undefined;
101 |   } catch (error) {
102 |     // If we can't read project ID, return undefined (backward compatibility)
103 |     return undefined;
104 |   }
105 | }
106 | 
107 | // Generate helpful error message for missing credentials
108 | export function generateCredentialsErrorMessage(): string {
109 |   return `
110 | OAuth credentials not found. Please provide credentials using one of these methods:
111 | 
112 | 1. Environment variable:
113 |    Set GOOGLE_OAUTH_CREDENTIALS to the path of your credentials file:
114 |    export GOOGLE_OAUTH_CREDENTIALS="/path/to/gcp-oauth.keys.json"
115 | 
116 | 2. Default file path:
117 |    Place your gcp-oauth.keys.json file in the package root directory.
118 | 
119 | Token storage:
120 | - Tokens are saved to: ${getSecureTokenPath()}
121 | - To use a custom token location, set GOOGLE_CALENDAR_MCP_TOKEN_PATH environment variable
122 | 
123 | To get OAuth credentials:
124 | 1. Go to the Google Cloud Console (https://console.cloud.google.com/)
125 | 2. Create or select a project
126 | 3. Enable the Google Calendar API
127 | 4. Create OAuth 2.0 credentials
128 | 5. Download the credentials file as gcp-oauth.keys.json
129 | `.trim();
130 | }
131 | 
```

--------------------------------------------------------------------------------
/src/tests/unit/utils/field-mask-builder.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from 'vitest';
  2 | import {
  3 |   buildEventFieldMask,
  4 |   buildSingleEventFieldMask,
  5 |   buildListFieldMask,
  6 |   validateFields,
  7 |   ALLOWED_EVENT_FIELDS,
  8 |   DEFAULT_EVENT_FIELDS
  9 | } from '../../../utils/field-mask-builder.js';
 10 | 
 11 | describe('Field Mask Builder', () => {
 12 |   describe('validateFields', () => {
 13 |     it('should accept valid fields', () => {
 14 |       const validFields = ['description', 'colorId', 'transparency'];
 15 |       const result = validateFields(validFields);
 16 |       expect(result).toEqual(validFields);
 17 |     });
 18 | 
 19 |     it('should reject invalid fields', () => {
 20 |       const invalidFields = ['invalid', 'notafield'];
 21 |       expect(() => validateFields(invalidFields)).toThrow('Invalid fields requested: invalid, notafield');
 22 |     });
 23 | 
 24 |     it('should handle mixed valid and invalid fields', () => {
 25 |       const mixedFields = ['description', 'invalid', 'colorId'];
 26 |       expect(() => validateFields(mixedFields)).toThrow('Invalid fields requested: invalid');
 27 |     });
 28 | 
 29 |     it('should accept all allowed fields', () => {
 30 |       const result = validateFields([...ALLOWED_EVENT_FIELDS]);
 31 |       expect(result).toEqual(ALLOWED_EVENT_FIELDS);
 32 |     });
 33 |   });
 34 | 
 35 |   describe('buildEventFieldMask', () => {
 36 |     it('should return undefined when no fields requested', () => {
 37 |       expect(buildEventFieldMask()).toBeUndefined();
 38 |       expect(buildEventFieldMask([])).toBeUndefined();
 39 |     });
 40 | 
 41 |     it('should build field mask with requested fields and defaults', () => {
 42 |       const fields = ['description', 'colorId'];
 43 |       const result = buildEventFieldMask(fields);
 44 |       expect(result).toContain('items(');
 45 |       expect(result).toContain('description');
 46 |       expect(result).toContain('colorId');
 47 |       // Should also include defaults
 48 |       DEFAULT_EVENT_FIELDS.forEach(field => {
 49 |         expect(result).toContain(field);
 50 |       });
 51 |     });
 52 | 
 53 |     it('should build field mask without defaults when specified', () => {
 54 |       const fields = ['description', 'colorId'];
 55 |       const result = buildEventFieldMask(fields, false);
 56 |       expect(result).toBe('items(description,colorId)');
 57 |     });
 58 | 
 59 |     it('should handle duplicate fields', () => {
 60 |       const fields = ['description', 'description', 'id', 'summary'];
 61 |       const result = buildEventFieldMask(fields);
 62 |       // Should deduplicate
 63 |       const fieldCount = (result?.match(/description/g) || []).length;
 64 |       expect(fieldCount).toBe(1);
 65 |     });
 66 | 
 67 |     it('should throw for invalid fields', () => {
 68 |       const fields = ['description', 'invalidfield'];
 69 |       expect(() => buildEventFieldMask(fields)).toThrow('Invalid fields requested: invalidfield');
 70 |     });
 71 |   });
 72 | 
 73 |   describe('buildSingleEventFieldMask', () => {
 74 |     it('should return undefined when no fields requested', () => {
 75 |       expect(buildSingleEventFieldMask()).toBeUndefined();
 76 |       expect(buildSingleEventFieldMask([])).toBeUndefined();
 77 |     });
 78 | 
 79 |     it('should build comma-separated field list with defaults', () => {
 80 |       const fields = ['description', 'colorId'];
 81 |       const result = buildSingleEventFieldMask(fields);
 82 |       expect(result).not.toContain('items(');
 83 |       expect(result).toContain('description');
 84 |       expect(result).toContain('colorId');
 85 |       // Should also include defaults
 86 |       DEFAULT_EVENT_FIELDS.forEach(field => {
 87 |         expect(result).toContain(field);
 88 |       });
 89 |     });
 90 | 
 91 |     it('should build field list without defaults when specified', () => {
 92 |       const fields = ['description', 'colorId'];
 93 |       const result = buildSingleEventFieldMask(fields, false);
 94 |       expect(result).toBe('description,colorId');
 95 |     });
 96 |   });
 97 | 
 98 |   describe('buildListFieldMask', () => {
 99 |     it('should return undefined when no fields requested', () => {
100 |       expect(buildListFieldMask()).toBeUndefined();
101 |       expect(buildListFieldMask([])).toBeUndefined();
102 |     });
103 | 
104 |     it('should include list metadata fields', () => {
105 |       const fields = ['description'];
106 |       const result = buildListFieldMask(fields);
107 |       expect(result).toContain('nextPageToken');
108 |       expect(result).toContain('nextSyncToken');
109 |       expect(result).toContain('kind');
110 |       expect(result).toContain('etag');
111 |       expect(result).toContain('timeZone');
112 |       expect(result).toContain('accessRole');
113 |     });
114 | 
115 |     it('should include event fields in items()', () => {
116 |       const fields = ['description', 'colorId'];
117 |       const result = buildListFieldMask(fields);
118 |       expect(result).toContain('items(');
119 |       expect(result).toContain('description');
120 |       expect(result).toContain('colorId');
121 |     });
122 |   });
123 | });
```

--------------------------------------------------------------------------------
/src/handlers/core/FreeBusyEventHandler.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { BaseToolHandler } from './BaseToolHandler.js';
  2 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
  3 | import { OAuth2Client } from "google-auth-library";
  4 | import { GetFreeBusyInput } from "../../tools/registry.js";
  5 | import { FreeBusyResponse as GoogleFreeBusyResponse } from '../../schemas/types.js';
  6 | import { FreeBusyResponse } from '../../types/structured-responses.js';
  7 | import { createStructuredResponse } from '../../utils/response-builder.js';
  8 | import { McpError } from '@modelcontextprotocol/sdk/types.js';
  9 | import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
 10 | import { convertToRFC3339 } from '../utils/datetime.js';
 11 | 
 12 | export class FreeBusyEventHandler extends BaseToolHandler {
 13 |   async runTool(args: any, oauth2Client: OAuth2Client): Promise<CallToolResult> {
 14 |     const validArgs = args as GetFreeBusyInput;
 15 | 
 16 |     if(!this.isLessThanThreeMonths(validArgs.timeMin,validArgs.timeMax)){
 17 |       throw new McpError(
 18 |         ErrorCode.InvalidRequest,
 19 |         "The time gap between timeMin and timeMax must be less than 3 months"
 20 |       );
 21 |     }
 22 | 
 23 |     const result = await this.queryFreeBusy(oauth2Client, validArgs);
 24 | 
 25 |     const response: FreeBusyResponse = {
 26 |       timeMin: validArgs.timeMin,
 27 |       timeMax: validArgs.timeMax,
 28 |       calendars: this.formatCalendarsData(result)
 29 |     };
 30 | 
 31 |     return createStructuredResponse(response);
 32 |   }
 33 | 
 34 |   private async queryFreeBusy(
 35 |     client: OAuth2Client,
 36 |     args: GetFreeBusyInput
 37 |   ): Promise<GoogleFreeBusyResponse> {
 38 |     try {
 39 |       const calendar = this.getCalendar(client);
 40 | 
 41 |       // Determine timezone with correct precedence:
 42 |       // 1. Explicit timeZone parameter (highest priority)
 43 |       // 2. Primary calendar's default timezone (fallback)
 44 |       // 3. UTC if calendar timezone retrieval fails
 45 |       let timezone: string;
 46 |       if (args.timeZone) {
 47 |         timezone = args.timeZone;
 48 |       } else {
 49 |         try {
 50 |           timezone = await this.getCalendarTimezone(client, 'primary');
 51 |         } catch (error) {
 52 |           // If we can't get the primary calendar's timezone, fall back to UTC
 53 |           // This can happen if the user doesn't have access to 'primary' calendar
 54 |           timezone = 'UTC';
 55 |         }
 56 |       }
 57 | 
 58 |       // Convert time boundaries to RFC3339 format for Google Calendar API
 59 |       // This handles both timezone-aware and timezone-naive datetime strings
 60 |       const timeMin = convertToRFC3339(args.timeMin, timezone);
 61 |       const timeMax = convertToRFC3339(args.timeMax, timezone);
 62 | 
 63 |       // Build request body
 64 |       // Note: The timeZone parameter affects the response format, not request interpretation
 65 |       // Since timeMin/timeMax are in RFC3339 (with timezone), they're unambiguous
 66 |       // But we include timeZone so busy periods in the response use consistent timezone
 67 |       const requestBody: any = {
 68 |         timeMin,
 69 |         timeMax,
 70 |         items: args.calendars,
 71 |         timeZone: timezone, // Always include to ensure response consistency
 72 |       };
 73 | 
 74 |       // Only add optional expansion fields if provided
 75 |       if (args.groupExpansionMax !== undefined) {
 76 |         requestBody.groupExpansionMax = args.groupExpansionMax;
 77 |       }
 78 |       if (args.calendarExpansionMax !== undefined) {
 79 |         requestBody.calendarExpansionMax = args.calendarExpansionMax;
 80 |       }
 81 | 
 82 |       const response = await calendar.freebusy.query({
 83 |         requestBody,
 84 |       });
 85 |       return response.data as GoogleFreeBusyResponse;
 86 |     } catch (error) {
 87 |       throw this.handleGoogleApiError(error);
 88 |     }
 89 |   }
 90 | 
 91 |   private isLessThanThreeMonths(timeMin: string, timeMax: string): boolean {
 92 |     const minDate = new Date(timeMin);
 93 |     const maxDate = new Date(timeMax);
 94 | 
 95 |     const diffInMilliseconds = maxDate.getTime() - minDate.getTime();
 96 |     const threeMonthsInMilliseconds = 3 * 30 * 24 * 60 * 60 * 1000;
 97 | 
 98 |     return diffInMilliseconds <= threeMonthsInMilliseconds;
 99 |   }
100 | 
101 |   private formatCalendarsData(response: GoogleFreeBusyResponse): Record<string, {
102 |     busy: Array<{ start: string; end: string }>;
103 |     errors?: Array<{ domain?: string; reason?: string }>;
104 |   }> {
105 |     const calendars: Record<string, any> = {};
106 | 
107 |     if (response.calendars) {
108 |       for (const [calId, calData] of Object.entries(response.calendars) as [string, any][]) {
109 |         calendars[calId] = {
110 |           busy: calData.busy?.map((slot: any) => ({
111 |             start: slot.start,
112 |             end: slot.end
113 |           })) || []
114 |         };
115 | 
116 |         if (calData.errors?.length > 0) {
117 |           calendars[calId].errors = calData.errors.map((err: any) => ({
118 |             domain: err.domain,
119 |             reason: err.reason
120 |           }));
121 |         }
122 |       }
123 |     }
124 | 
125 |     return calendars;
126 |   }
127 | }
128 | 
```

--------------------------------------------------------------------------------
/docs/advanced-usage.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Advanced Usage Guide
  2 | 
  3 | This guide covers advanced features and use cases for the Google Calendar MCP Server.
  4 | 
  5 | ## Multi-Account Support
  6 | 
  7 | The server supports managing multiple Google accounts (e.g., personal and a test calendar).
  8 | 
  9 | ### Setup Multiple Accounts
 10 | 
 11 | ```bash
 12 | # Authenticate normal account
 13 | npm run auth
 14 | 
 15 | # Authenticate test account
 16 | npm run auth:test
 17 | 
 18 | # Check status
 19 | npm run account:status
 20 | ```
 21 | 
 22 | ### Account Management Commands
 23 | 
 24 | ```bash
 25 | npm run account:clear:normal    # Clear normal account tokens
 26 | npm run account:clear:test      # Clear test account tokens
 27 | npm run account:migrate         # Migrate from old token format
 28 | ```
 29 | 
 30 | ### Using Multiple Accounts
 31 | 
 32 | The server intelligently determines which account to use:
 33 | - Normal operations use your primary account
 34 | - Integration tests automatically use the test account
 35 | - Accounts are isolated and secure
 36 | 
 37 | ## Batch Operations
 38 | 
 39 | ### List Events from Multiple Calendars
 40 | 
 41 | Request events from several calendars simultaneously:
 42 | 
 43 | ```
 44 | "Show me all events from my work, personal, and team calendars for next week"
 45 | ```
 46 | 
 47 | The server will:
 48 | 1. Query all specified calendars in parallel
 49 | 2. Merge and sort results chronologically
 50 | 3. Handle different timezones correctly
 51 | 
 52 | ### Batch Event Creation
 53 | 
 54 | Create multiple related events:
 55 | 
 56 | ```
 57 | "Schedule a 3-part training series every Monday at 2pm for the next 3 weeks"
 58 | ```
 59 | 
 60 | ## Recurring Events
 61 | 
 62 | ### Modification Scopes
 63 | 
 64 | When updating recurring events, you can specify the scope:
 65 | 
 66 | 1. **This event only**: Modify a single instance
 67 |    ```
 68 |    "Move tomorrow's standup to 11am (just this one)"
 69 |    ```
 70 | 
 71 | 2. **This and following events**: Modify from a specific date forward
 72 |    ```
 73 |    "Change all future team meetings to 30 minutes starting next week"
 74 |    ```
 75 | 
 76 | 3. **All events**: Modify the entire series
 77 |    ```
 78 |    "Update the location for all weekly reviews to Conference Room B"
 79 |    ```
 80 | 
 81 | ### Complex Recurrence Patterns
 82 | 
 83 | The server supports all Google Calendar recurrence rules:
 84 | - Daily, weekly, monthly, yearly patterns
 85 | - Custom intervals (e.g., every 3 days)
 86 | - Specific days (e.g., every Tuesday and Thursday)
 87 | - End conditions (after N occurrences or by date)
 88 | 
 89 | ## Timezone Handling
 90 | 
 91 | All times require explicit timezone information:
 92 | - Automatic timezone detection based on your calendar settings
 93 | - Support for scheduling across timezones
 94 | - Proper handling of daylight saving time transitions
 95 | 
 96 | ### Availability Checking
 97 | 
 98 | Find optimal meeting times:
 99 | 
100 | ```
101 | "Find a 90-minute slot next week when both my work and personal calendars are free, preferably in the afternoon"
102 | ```
103 | 
104 | ## Working with Images
105 | 
106 | ### Extract Events from Screenshots
107 | 
108 | ```
109 | "Add this event to my calendar [attach screenshot]"
110 | ```
111 | 
112 | Supported formats: PNG, JPEG, GIF
113 | 
114 | The server can extract:
115 | - Date and time information
116 | - Event titles and descriptions
117 | - Location details
118 | - Attendee lists
119 | 
120 | ### Best Practices for Image Recognition
121 | 
122 | 1. Ensure text is clear and readable
123 | 2. Include full date/time information in the image
124 | 3. Highlight or circle important details
125 | 4. Use high contrast images
126 | 
127 | ## Advanced Search
128 | 
129 | ### Search Operators
130 | 
131 | - **By attendee**: "meetings with [email protected]"
132 | - **By location**: "events at headquarters"
133 | - **By time range**: "morning meetings this month"
134 | - **By status**: "tentative events this week"
135 | 
136 | ### Complex Queries
137 | 
138 | Combine multiple criteria:
139 | 
140 | ```
141 | "Find all meetings with the sales team at the main office that are longer than an hour in the next two weeks"
142 | ```
143 | 
144 | ## Calendar Analysis
145 | 
146 | ### Meeting Patterns
147 | 
148 | ```
149 | "How much time did I spend in meetings last week?"
150 | "What percentage of my meetings are recurring?"
151 | "Which day typically has the most meetings?"
152 | ```
153 | ## Performance Optimization
154 | 
155 | ### Rate Limiting
156 | 
157 | Built-in protection against API limits:
158 | - Automatic retry with exponential backoff in batch operations
159 | - HTTP transport includes basic rate limiting (100 requests per IP per 15 minutes)
160 | 
161 | ## Integration Examples
162 | 
163 | ### Daily Schedule
164 | 
165 | ```
166 | "Show me today's events and check for any scheduling conflicts between all my calendars"
167 | ```
168 | 
169 | ### Weekly Planning
170 | 
171 | ```
172 | "Look at next week and suggest the best times for deep work blocks of at least 2 hours"
173 | ```
174 | 
175 | ### Meeting Preparation
176 | 
177 | ```
178 | "For each meeting tomorrow, tell me who's attending, what the agenda is, and what materials I should review"
179 | ```
180 | 
181 | ## Security Considerations
182 | 
183 | ### Permission Scopes
184 | 
185 | The server only requests necessary permissions:
186 | - `calendar.events`: Full event management
187 | - Never requests email or profile access
188 | - No access to other Google services
189 | 
190 | ### Token Security
191 | 
192 | - Tokens encrypted at rest
193 | - Automatic token refresh
194 | - Secure credential storage
195 | - No tokens in logs or debug output
196 | 
197 | ## Debugging
198 | 
199 | ### Enable Debug Logging
200 | 
201 | ```bash
202 | DEBUG=mcp:* npm start
203 | ```
204 | 
205 | ### Common Issues
206 | 
207 | 1. **Token refresh failures**: Check network connectivity
208 | 2. **API quota exceeded**: Implement backoff strategies
209 | 3. **Timezone mismatches**: Ensure consistent timezone usage
210 | 
211 | See [Troubleshooting Guide](troubleshooting.md) for detailed solutions.
```

--------------------------------------------------------------------------------
/src/handlers/utils/datetime.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Datetime utilities for Google Calendar MCP Server
  3 |  * Provides timezone handling and datetime conversion utilities
  4 |  */
  5 | 
  6 | /**
  7 |  * Checks if a datetime string includes timezone information
  8 |  * @param datetime ISO 8601 datetime string
  9 |  * @returns True if timezone is included, false if timezone-naive
 10 |  */
 11 | export function hasTimezoneInDatetime(datetime: string): boolean {
 12 |     return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(datetime);
 13 | }
 14 | 
 15 | /**
 16 |  * Converts a flexible datetime string to RFC3339 format required by Google Calendar API
 17 |  * 
 18 |  * Precedence rules:
 19 |  * 1. If datetime already has timezone info (Z or ±HH:MM), use as-is
 20 |  * 2. If datetime is timezone-naive, interpret it as local time in fallbackTimezone and convert to UTC
 21 |  * 
 22 |  * @param datetime ISO 8601 datetime string (with or without timezone)
 23 |  * @param fallbackTimezone Timezone to use if datetime is timezone-naive (IANA format)
 24 |  * @returns RFC3339 formatted datetime string in UTC
 25 |  */
 26 | export function convertToRFC3339(datetime: string, fallbackTimezone: string): string {
 27 |     if (hasTimezoneInDatetime(datetime)) {
 28 |         // Already has timezone, use as-is
 29 |         return datetime;
 30 |     } else {
 31 |         // Timezone-naive, interpret as local time in fallbackTimezone and convert to UTC
 32 |         try {
 33 |             // Parse the datetime components
 34 |             const match = datetime.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})$/);
 35 |             if (!match) {
 36 |                 throw new Error('Invalid datetime format');
 37 |             }
 38 |             
 39 |             const [, year, month, day, hour, minute, second] = match.map(Number);
 40 |             
 41 |             // Create a temporary date in UTC to get the baseline
 42 |             const utcDate = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
 43 |             
 44 |             // Find what UTC time corresponds to the desired local time in the target timezone
 45 |             // We do this by binary search approach or by using the timezone offset
 46 |             const targetDate = convertLocalTimeToUTC(year, month - 1, day, hour, minute, second, fallbackTimezone);
 47 |             
 48 |             return targetDate.toISOString().replace(/\.000Z$/, 'Z');
 49 |         } catch (error) {
 50 |             // Fallback: if timezone conversion fails, append Z for UTC
 51 |             return datetime + 'Z';
 52 |         }
 53 |     }
 54 | }
 55 | 
 56 | /**
 57 |  * Convert a local time in a specific timezone to UTC
 58 |  */
 59 | function convertLocalTimeToUTC(year: number, month: number, day: number, hour: number, minute: number, second: number, timezone: string): Date {
 60 |     // Create a date that we'll use to find the correct UTC time
 61 |     // Start with the assumption that it's in UTC
 62 |     let testDate = new Date(Date.UTC(year, month, day, hour, minute, second));
 63 |     
 64 |     // Get what this UTC time looks like in the target timezone
 65 |     const options: Intl.DateTimeFormatOptions = {
 66 |         timeZone: timezone,
 67 |         year: 'numeric',
 68 |         month: '2-digit',
 69 |         day: '2-digit',
 70 |         hour: '2-digit',
 71 |         minute: '2-digit',
 72 |         second: '2-digit',
 73 |         hour12: false
 74 |     };
 75 |     
 76 |     // Format the test date in the target timezone
 77 |     const formatter = new Intl.DateTimeFormat('sv-SE', options);
 78 |     const formattedInTargetTZ = formatter.format(testDate);
 79 |     
 80 |     // Parse the formatted result to see what time it shows
 81 |     const [datePart, timePart] = formattedInTargetTZ.split(' ');
 82 |     const [targetYear, targetMonth, targetDay] = datePart.split('-').map(Number);
 83 |     const [targetHour, targetMinute, targetSecond] = timePart.split(':').map(Number);
 84 |     
 85 |     // Calculate the difference between what we want and what we got
 86 |     const wantedTime = new Date(year, month, day, hour, minute, second).getTime();
 87 |     const actualTime = new Date(targetYear, targetMonth - 1, targetDay, targetHour, targetMinute, targetSecond).getTime();
 88 |     const offsetMs = wantedTime - actualTime;
 89 |     
 90 |     // Adjust the UTC time by the offset
 91 |     return new Date(testDate.getTime() + offsetMs);
 92 | }
 93 | 
 94 | /**
 95 |  * Creates a time object for Google Calendar API, handling both timezone-aware and timezone-naive datetime strings
 96 |  * Also handles all-day events by using 'date' field instead of 'dateTime'
 97 |  * @param datetime ISO 8601 datetime string (with or without timezone)
 98 |  * @param fallbackTimezone Timezone to use if datetime is timezone-naive (IANA format)
 99 |  * @returns Google Calendar API time object
100 |  */
101 | export function createTimeObject(datetime: string, fallbackTimezone: string): { dateTime?: string; date?: string; timeZone?: string } {
102 |     // Check if this is a date-only string (all-day event)
103 |     // Date-only format: YYYY-MM-DD (no time component)
104 |     if (!/T/.test(datetime)) {
105 |         // This is a date-only string, use the 'date' field for all-day event
106 |         return { date: datetime };
107 |     }
108 |     
109 |     // This is a datetime string with time component
110 |     if (hasTimezoneInDatetime(datetime)) {
111 |         // Timezone included in datetime - use as-is, no separate timeZone property needed
112 |         return { dateTime: datetime };
113 |     } else {
114 |         // Timezone-naive datetime - use fallback timezone
115 |         return { dateTime: datetime, timeZone: fallbackTimezone };
116 |     }
117 | }
```

--------------------------------------------------------------------------------
/docs/authentication.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Authentication Setup Guide
  2 | 
  3 | This guide provides detailed instructions for setting up Google OAuth 2.0 authentication for the Google Calendar MCP Server.
  4 | 
  5 | ## Google Cloud Setup
  6 | 
  7 | ### 1. Create a Google Cloud Project
  8 | 
  9 | 1. Go to the [Google Cloud Console](https://console.cloud.google.com)
 10 | 2. Click "Select a project" → "New Project"
 11 | 3. Enter a project name (e.g., "Calendar MCP")
 12 | 4. Click "Create"
 13 | 
 14 | ### 2. Enable the Google Calendar API
 15 | 
 16 | 1. In your project, go to "APIs & Services" → "Library"
 17 | 2. Search for "Google Calendar API"
 18 | 3. Click on it and press "Enable"
 19 | 4. Wait for the API to be enabled (usually takes a few seconds)
 20 | 
 21 | ### 3. Create OAuth 2.0 Credentials
 22 | 
 23 | 1. Go to "APIs & Services" → "Credentials"
 24 | 2. Click "Create Credentials" → "OAuth client ID"
 25 | 3. If prompted, configure the OAuth consent screen first:
 26 |    - Choose "External" user type
 27 |    - Fill in the required fields:
 28 |      - App name: "Calendar MCP" (or your choice)
 29 |      - User support email: Your email
 30 |      - Developer contact: Your email
 31 |    - Add scopes:
 32 |      - Click "Add or Remove Scopes"
 33 |      - Add: `https://www.googleapis.com/auth/calendar.events`
 34 |      - Or use the broader scope: `https://www.googleapis.com/auth/calendar`
 35 |    - Add test users:
 36 |      - Add your email address
 37 |      - **Important**: Wait 2-3 minutes for test users to propagate
 38 | 
 39 | 4. Create the OAuth client:
 40 |    - Application type: **Desktop app** (Important!)
 41 |    - Name: "Calendar MCP Client"
 42 |    - Click "Create"
 43 | 
 44 | 5. Download the credentials:
 45 |    - Click the download button (⬇️) next to your new client
 46 |    - Save as `gcp-oauth.keys.json`
 47 | 
 48 | ## Credential File Format
 49 | 
 50 | Your credentials file should look like this:
 51 | 
 52 | ```json
 53 | {
 54 |   "installed": {
 55 |     "client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",
 56 |     "client_secret": "YOUR_CLIENT_SECRET",
 57 |     "auth_uri": "https://accounts.google.com/o/oauth2/auth",
 58 |     "token_uri": "https://oauth2.googleapis.com/token",
 59 |     "redirect_uris": ["http://localhost"]
 60 |   }
 61 | }
 62 | ```
 63 | 
 64 | ## Credential Storage Options
 65 | 
 66 | ### Option 1: Environment Variable (Recommended)
 67 | 
 68 | Set the `GOOGLE_OAUTH_CREDENTIALS` environment variable to point to your credentials file:
 69 | 
 70 | ```bash
 71 | export GOOGLE_OAUTH_CREDENTIALS="/path/to/your/gcp-oauth.keys.json"
 72 | ```
 73 | 
 74 | In Claude Desktop config:
 75 | ```json
 76 | {
 77 |   "mcpServers": {
 78 |     "google-calendar": {
 79 |       "command": "npx",
 80 |       "args": ["@cocal/google-calendar-mcp"],
 81 |       "env": {
 82 |         "GOOGLE_OAUTH_CREDENTIALS": "/path/to/your/gcp-oauth.keys.json"
 83 |       }
 84 |     }
 85 |   }
 86 | }
 87 | ```
 88 | 
 89 | ### Option 2: Default Location
 90 | 
 91 | Place the credentials file in the project root as `gcp-oauth.keys.json`.
 92 | 
 93 | ## Token Storage
 94 | 
 95 | OAuth tokens are automatically stored in a secure location:
 96 | 
 97 | - **macOS/Linux**: `~/.config/google-calendar-mcp/tokens.json`
 98 | - **Windows**: `%APPDATA%\google-calendar-mcp\tokens.json`
 99 | 
100 | To use a custom location, set:
101 | ```bash
102 | export GOOGLE_CALENDAR_MCP_TOKEN_PATH="/custom/path/tokens.json"
103 | ```
104 | 
105 | ## First-Time Authentication
106 | 
107 | 1. Start Claude Desktop after configuration
108 | 2. The server will automatically open your browser for authentication
109 | 3. Sign in with your Google account
110 | 4. Grant the requested calendar permissions
111 | 5. You'll see a success message in the browser
112 | 6. Return to Claude - you're ready to use calendar features!
113 | 
114 | ## Re-authentication
115 | 
116 | If your tokens expire or become invalid:
117 | 
118 | ### For NPX Installation
119 | If you're using the MCP via `npx` (e.g., in Claude Desktop):
120 | 
121 | ```bash
122 | # Set your credentials path first
123 | export GOOGLE_OAUTH_CREDENTIALS="/path/to/your/gcp-oauth.keys.json"
124 | 
125 | # Run the auth command
126 | npx @cocal/google-calendar-mcp auth
127 | ```
128 | 
129 | ### For Local Installation
130 | ```bash
131 | npm run auth
132 | ```
133 | 
134 | The server will guide you through the authentication flow again.
135 | 
136 | ## Important Notes
137 | 
138 | ### Test Mode Limitations
139 | 
140 | While your app is in test mode:
141 | - OAuth tokens expire after 7 days
142 | - Limited to test users you've explicitly added
143 | - Perfect for personal use
144 | 
145 | ### Avoiding Token Expiration
146 | 
147 | Test mode tokens expire after 7 days. For personal use, you can simply re-authenticate weekly using the commands above. 
148 | 
149 | If you need longer-lived tokens, you can publish your app to production mode in Google Cloud Console. The the number of users will be restricted unless the application completes a full approval review. Google will also warn that the app is unverified and required bypassing a warning screen. 
150 | 
151 | 
152 | ### Security Best Practices
153 | 
154 | 1. **Never commit credentials**: Add `gcp-oauth.keys.json` to `.gitignore`
155 | 2. **Secure file permissions**: 
156 |    ```bash
157 |    chmod 600 /path/to/gcp-oauth.keys.json
158 |    ```
159 | 3. **Use environment variables**: Keeps credentials out of config files
160 | 4. **Regularly rotate**: Regenerate credentials if compromised
161 | 
162 | ## Troubleshooting
163 | 
164 | ### "Invalid credentials" error
165 | - Ensure you selected "Desktop app" as the application type
166 | - Check that the credentials file is valid JSON
167 | - Verify the file path is correct
168 | 
169 | ### "Access blocked" error
170 | - Add your email as a test user in OAuth consent screen
171 | - Wait 2-3 minutes for changes to propagate
172 | 
173 | ### "Token expired" error
174 | - Run `npm run auth` to re-authenticate
175 | - Check if you're in test mode (7-day expiration)
176 | 
177 | See [Troubleshooting Guide](troubleshooting.md) for more issues and solutions.
```

--------------------------------------------------------------------------------
/examples/http-client.js:
--------------------------------------------------------------------------------

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * Modern HTTP Client for Google Calendar MCP Server
  5 |  * 
  6 |  * This demonstrates how to connect to the Google Calendar MCP server
  7 |  * when it's running in StreamableHTTP transport mode. To test this
  8 |  * make sure you have the server running locally `npm run start:http`
  9 |  */
 10 | 
 11 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 12 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
 13 | 
 14 | async function main() {
 15 |   const serverUrl = process.argv[2] || 'http://localhost:3000';
 16 |   
 17 |   console.log(`🔗 Connecting to Google Calendar MCP Server at: ${serverUrl}`);
 18 |   
 19 |   try {
 20 |     // First test health endpoint to ensure server is running
 21 |     console.log('🏥 Testing server health...');
 22 |     const healthResponse = await fetch(`${serverUrl}/health`, {
 23 |       headers: {
 24 |         'Accept': 'application/json'
 25 |       }
 26 |     });
 27 |     
 28 |     if (healthResponse.ok) {
 29 |       const healthData = await healthResponse.json();
 30 |       console.log('✅ Server is healthy:', healthData);
 31 |     } else {
 32 |       console.error('❌ Server health check failed');
 33 |       return;
 34 |     }
 35 | 
 36 |     // Create MCP client
 37 |     const client = new Client({
 38 |       name: "google-calendar-http-client",
 39 |       version: "1.0.0"
 40 |     }, {
 41 |       capabilities: {
 42 |         tools: {}
 43 |       }
 44 |     });
 45 | 
 46 |     // Skip direct initialization test to avoid double-initialization error
 47 |     console.log('\n🔍 Skipping direct initialization test...');
 48 | 
 49 |     // Connect with SDK transport
 50 |     console.log('\n🚀 Connecting with MCP SDK transport...');
 51 |     const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
 52 |     
 53 |     await client.connect(transport);
 54 |     console.log('✅ Connected to server');
 55 | 
 56 |     // List available tools
 57 |     console.log('\n📋 Listing available tools...');
 58 |     const tools = await client.listTools();
 59 |     console.log(`Found ${tools.tools.length} tools:`);
 60 |     
 61 |     tools.tools.forEach((tool, index) => {
 62 |       console.log(`  ${index + 1}. ${tool.name}`);
 63 |       console.log(`     Description: ${tool.description}`);
 64 |     });
 65 | 
 66 |     // Test some basic tools
 67 |     console.log('\n🛠️ Testing tools...');
 68 |     
 69 |     // Test list-calendars
 70 |     try {
 71 |       console.log('\n📅 Testing list-calendars...');
 72 |       const calendarsResult = await client.callTool({
 73 |         name: 'list-calendars',
 74 |         arguments: {}
 75 |       });
 76 |       console.log('✅ list-calendars successful');
 77 |       console.log('Result:', calendarsResult.content[0].text.substring(0, 300) + '...');
 78 |     } catch (error) {
 79 |       console.log('❌ list-calendars failed:', error.message);
 80 |     }
 81 | 
 82 |     // Test list-colors
 83 |     try {
 84 |       console.log('\n🎨 Testing list-colors...');
 85 |       const colorsResult = await client.callTool({
 86 |         name: 'list-colors',
 87 |         arguments: {}
 88 |       });
 89 |       console.log('✅ list-colors successful');
 90 |       console.log('Result:', colorsResult.content[0].text.substring(0, 300) + '...');
 91 |     } catch (error) {
 92 |       console.log('❌ list-colors failed:', error.message);
 93 |     }
 94 | 
 95 |     // Test list-events for primary calendar
 96 |     try {
 97 |       console.log('\n📆 Testing list-events...');
 98 |       
 99 |       // Create ISO strings using standard JavaScript toISOString()
100 |       const now = new Date();
101 |       const nextWeek = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
102 |       
103 |       // Use standard RFC 3339 format with milliseconds (now properly supported)
104 |       const timeMin = now.toISOString().split('.')[0] + 'Z';
105 |       const timeMax = nextWeek.toISOString().split('.')[0] + 'Z';
106 |       
107 |       console.log(`📅 Fetching events from ${timeMin} to ${timeMax}`);
108 |       
109 |       const eventsResult = await client.callTool({
110 |         name: 'list-events',
111 |         arguments: {
112 |           calendarId: 'primary',
113 |           timeMin: timeMin,
114 |           timeMax: timeMax
115 |         }
116 |       });
117 |       console.log('✅ list-events successful');
118 |       console.log('Result:', eventsResult.content[0].text.substring(0, 500) + '...');
119 |     } catch (error) {
120 |       console.log('❌ list-events failed:', error.message);
121 |     }
122 | 
123 |     // Close the connection
124 |     console.log('\n🔒 Closing connection...');
125 |     await client.close();
126 |     console.log('✅ Connection closed');
127 |     
128 |     console.log('\n🎉 Google Calendar MCP client test completed!');
129 | 
130 |   } catch (error) {
131 |     console.error('❌ Error:', error);
132 |     
133 |     if (error.message.includes('Authentication required')) {
134 |       console.log('\n💡 Authentication required:');
135 |       console.log('   Run: npm run auth');
136 |       console.log('   Then restart the server: npm run start:http');
137 |     } else if (error.message.includes('ECONNREFUSED')) {
138 |       console.log('\n💡 Server not running:');
139 |       console.log('   Start server: npm run start:http');
140 |     } else {
141 |       console.log('\n💡 Check that:');
142 |       console.log('   1. Server is running (npm run start:http)');
143 |       console.log('   2. Authentication is complete (npm run auth)');
144 |       console.log('   3. Server URL is correct');
145 |     }
146 |     
147 |     process.exit(1);
148 |   }
149 | }
150 | 
151 | // Handle graceful shutdown
152 | process.on('SIGINT', () => {
153 |   console.log('\n👋 Shutting down HTTP client...');
154 |   process.exit(0);
155 | });
156 | 
157 | main().catch(error => {
158 |   console.error('❌ Unhandled error:', error);
159 |   process.exit(1);
160 | });
161 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { fileURLToPath } from "url";
  2 | import { GoogleCalendarMcpServer } from './server.js';
  3 | import { parseArgs } from './config/TransportConfig.js';
  4 | import { readFileSync } from "fs";
  5 | import { join, dirname } from "path";
  6 | 
  7 | // Import modular components
  8 | import { initializeOAuth2Client } from './auth/client.js';
  9 | import { AuthServer } from './auth/server.js';
 10 | 
 11 | // Get package version
 12 | const __filename = fileURLToPath(import.meta.url);
 13 | const __dirname = dirname(__filename);
 14 | const packageJsonPath = join(__dirname, '..', 'package.json');
 15 | const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
 16 | const VERSION = packageJson.version;
 17 | 
 18 | // --- Main Application Logic --- 
 19 | async function main() {
 20 |   try {
 21 |     // Parse command line arguments
 22 |     const config = parseArgs(process.argv.slice(2));
 23 |     
 24 |     // Create and initialize the server
 25 |     const server = new GoogleCalendarMcpServer(config);
 26 |     await server.initialize();
 27 |     
 28 |     // Start the server with the appropriate transport
 29 |     await server.start();
 30 | 
 31 |   } catch (error: unknown) {
 32 |     process.stderr.write(`Failed to start server: ${error instanceof Error ? error.message : error}\n`);
 33 |     process.exit(1);
 34 |   }
 35 | }
 36 | 
 37 | 
 38 | // --- Command Line Interface ---
 39 | async function runAuthServer(): Promise<void> {
 40 |   // Use the same logic as auth-server.ts
 41 |   try {
 42 |     // Initialize OAuth client
 43 |     const oauth2Client = await initializeOAuth2Client();
 44 | 
 45 |     // Create and start the auth server
 46 |     const authServerInstance = new AuthServer(oauth2Client);
 47 | 
 48 |     // Start with browser opening (true by default)
 49 |     const success = await authServerInstance.start(true);
 50 | 
 51 |     if (!success && !authServerInstance.authCompletedSuccessfully) {
 52 |       // Failed to start and tokens weren't already valid
 53 |       process.stderr.write(
 54 |         "Authentication failed. Could not start server or validate existing tokens. Check port availability (3000-3004) and try again.\n"
 55 |       );
 56 |       process.exit(1);
 57 |     } else if (authServerInstance.authCompletedSuccessfully) {
 58 |       // Auth was successful (either existing tokens were valid or flow completed just now)
 59 |       process.stderr.write("Authentication successful.\n");
 60 |       process.exit(0); // Exit cleanly if auth is already done
 61 |     }
 62 | 
 63 |     // If we reach here, the server started and is waiting for the browser callback
 64 |     process.stderr.write(
 65 |       "Authentication server started. Please complete the authentication in your browser...\n"
 66 |     );
 67 | 
 68 |     // Wait for completion
 69 |     const intervalId = setInterval(async () => {
 70 |       if (authServerInstance.authCompletedSuccessfully) {
 71 |         clearInterval(intervalId);
 72 |         await authServerInstance.stop();
 73 |         process.stderr.write("Authentication completed successfully!\n");
 74 |         process.exit(0);
 75 |       }
 76 |     }, 1000);
 77 |   } catch (error) {
 78 |     process.stderr.write(`Authentication failed: ${error}\n`);
 79 |     process.exit(1);
 80 |   }
 81 | }
 82 | 
 83 | function showHelp(): void {
 84 |   process.stdout.write(`
 85 | Google Calendar MCP Server v${VERSION}
 86 | 
 87 | Usage:
 88 |   npx @cocal/google-calendar-mcp [command]
 89 | 
 90 | Commands:
 91 |   auth     Run the authentication flow
 92 |   start    Start the MCP server (default)
 93 |   version  Show version information
 94 |   help     Show this help message
 95 | 
 96 | Examples:
 97 |   npx @cocal/google-calendar-mcp auth
 98 |   npx @cocal/google-calendar-mcp start
 99 |   npx @cocal/google-calendar-mcp version
100 |   npx @cocal/google-calendar-mcp
101 | 
102 | Environment Variables:
103 |   GOOGLE_OAUTH_CREDENTIALS    Path to OAuth credentials file
104 | `);
105 | }
106 | 
107 | function showVersion(): void {
108 |   process.stdout.write(`Google Calendar MCP Server v${VERSION}\n`);
109 | }
110 | 
111 | // --- Exports & Execution Guard --- 
112 | // Export main for testing or potential programmatic use
113 | export { main, runAuthServer };
114 | 
115 | // Parse CLI arguments
116 | function parseCliArgs(): { command: string | undefined } {
117 |   const args = process.argv.slice(2);
118 |   let command: string | undefined;
119 | 
120 |   for (let i = 0; i < args.length; i++) {
121 |     const arg = args[i];
122 |     
123 |     // Handle special version/help flags as commands
124 |     if (arg === '--version' || arg === '-v' || arg === '--help' || arg === '-h') {
125 |       command = arg;
126 |       continue;
127 |     }
128 |     
129 |     // Skip transport options and their values
130 |     if (arg === '--transport' || arg === '--port' || arg === '--host') {
131 |       i++; // Skip the next argument (the value)
132 |       continue;
133 |     }
134 |     
135 |     // Skip other flags
136 |     if (arg === '--debug') {
137 |       continue;
138 |     }
139 |     
140 |     // Check for command (first non-option argument)
141 |     if (!command && !arg.startsWith('--')) {
142 |       command = arg;
143 |       continue;
144 |     }
145 |   }
146 | 
147 |   return { command };
148 | }
149 | 
150 | // CLI logic here (run always)
151 | const { command } = parseCliArgs();
152 | 
153 | switch (command) {
154 |   case "auth":
155 |     runAuthServer().catch((error) => {
156 |       process.stderr.write(`Authentication failed: ${error}\n`);
157 |       process.exit(1);
158 |     });
159 |     break;
160 |   case "start":
161 |   case void 0:
162 |     main().catch((error) => {
163 |       process.stderr.write(`Failed to start server: ${error}\n`);
164 |       process.exit(1);
165 |     });
166 |     break;
167 |   case "version":
168 |   case "--version":
169 |   case "-v":
170 |     showVersion();
171 |     break;
172 |   case "help":
173 |   case "--help":
174 |   case "-h":
175 |     showHelp();
176 |     break;
177 |   default:
178 |     process.stderr.write(`Unknown command: ${command}\n`);
179 |     showHelp();
180 |     process.exit(1);
181 | }
```

--------------------------------------------------------------------------------
/examples/http-with-curl.sh:
--------------------------------------------------------------------------------

```bash
  1 | #!/bin/bash
  2 | 
  3 | # Test script for Google Calendar MCP Server HTTP mode using curl
  4 | # This demonstrates basic HTTP requests to test the MCP server
  5 | 
  6 | SERVER_URL="${1:-http://localhost:3000}"
  7 | SESSION_ID="curl-test-session-$(date +%s)"
  8 | 
  9 | echo "🚀 Testing Google Calendar MCP Server at: $SERVER_URL"
 10 | echo "🆔 Using session ID: $SESSION_ID"
 11 | echo "=================================================="
 12 | 
 13 | # Test 1: Health check
 14 | echo -e "\n🏥 Testing health endpoint..."
 15 | curl -s "$SERVER_URL/health" | jq '.' || echo "Health check failed"
 16 | 
 17 | # Test 2: Initialize MCP session
 18 | echo -e "\n🤝 Testing MCP initialize..."
 19 | 
 20 | # MCP Initialize request
 21 | INIT_REQUEST='{
 22 |   "jsonrpc": "2.0",
 23 |   "id": 1,
 24 |   "method": "initialize",
 25 |   "params": {
 26 |     "protocolVersion": "2024-11-05",
 27 |     "capabilities": {
 28 |       "tools": {}
 29 |     },
 30 |     "clientInfo": {
 31 |       "name": "curl-test-client",
 32 |       "version": "1.0.0"
 33 |     }
 34 |   }
 35 | }'
 36 | 
 37 | echo "Sending initialize request..."
 38 | INIT_RESPONSE=$(curl -s -X POST "$SERVER_URL" \
 39 |   -H "Content-Type: application/json" \
 40 |   -H "Accept: application/json, text/event-stream" \
 41 |   -H "mcp-session-id: $SESSION_ID" \
 42 |   -d "$INIT_REQUEST")
 43 | 
 44 | # Try to parse as JSON, if that fails, check if it's SSE format
 45 | if echo "$INIT_RESPONSE" | jq '.' >/dev/null 2>&1; then
 46 |   # Direct JSON response - extract and parse the nested content
 47 |   echo "$INIT_RESPONSE" | jq -r '.result.content[0].text // empty' | jq '.' 2>/dev/null || echo "$INIT_RESPONSE" | jq '.'
 48 | elif echo "$INIT_RESPONSE" | grep -q "^data:"; then
 49 |   # SSE format - extract data and parse nested content
 50 |   echo "$INIT_RESPONSE" | grep "^data:" | sed 's/^data: //' | jq -r '.result.content[0].text // empty' | jq '.' 2>/dev/null
 51 | else
 52 |   echo "❌ Unknown response format"
 53 |   echo "$INIT_RESPONSE"
 54 | fi
 55 | 
 56 | # Check if initialization was successful
 57 | if echo "$INIT_RESPONSE" | grep -q "result\|initialize"; then
 58 |   echo "✅ Initialization successful"
 59 | else
 60 |   echo "❌ Initialization failed - stopping tests"
 61 |   exit 1
 62 | fi
 63 | 
 64 | # Test 3: List Tools request (after successful initialization)
 65 | echo -e "\n📋 Testing list tools..."
 66 | LIST_TOOLS_REQUEST='{
 67 |   "jsonrpc": "2.0",
 68 |   "id": 2,
 69 |   "method": "tools/list",
 70 |   "params": {}
 71 | }'
 72 | 
 73 | TOOLS_RESPONSE=$(curl -s -X POST "$SERVER_URL" \
 74 |   -H "Content-Type: application/json" \
 75 |   -H "Accept: application/json, text/event-stream" \
 76 |   -H "mcp-session-id: $SESSION_ID" \
 77 |   -d "$LIST_TOOLS_REQUEST")
 78 | 
 79 | # Parse response appropriately
 80 | if echo "$TOOLS_RESPONSE" | jq '.' >/dev/null 2>&1; then
 81 |   # Direct JSON - show the full tools list response (not nested content)
 82 |   echo "$TOOLS_RESPONSE" | jq '.result.tools[] | {name, description}'
 83 | elif echo "$TOOLS_RESPONSE" | grep -q "^data:"; then
 84 |   # SSE format
 85 |   echo "$TOOLS_RESPONSE" | grep "^data:" | sed 's/^data: //' | jq '.result.tools[] | {name, description}'
 86 | else
 87 |   echo "❌ List tools failed - unknown format"
 88 |   echo "$TOOLS_RESPONSE"
 89 | fi
 90 | 
 91 | # Test 4: Call list-calendars tool
 92 | echo -e "\n📅 Testing list-calendars tool..."
 93 | 
 94 | LIST_CALENDARS_REQUEST='{
 95 |   "jsonrpc": "2.0",
 96 |   "id": 3,
 97 |   "method": "tools/call",
 98 |   "params": {
 99 |     "name": "list-calendars",
100 |     "arguments": {}
101 |   }
102 | }'
103 | 
104 | CALENDARS_RESPONSE=$(curl -s -X POST "$SERVER_URL" \
105 |   -H "Content-Type: application/json" \
106 |   -H "Accept: application/json, text/event-stream" \
107 |   -H "mcp-session-id: $SESSION_ID" \
108 |   -d "$LIST_CALENDARS_REQUEST")
109 | 
110 | # Parse response appropriately
111 | if echo "$CALENDARS_RESPONSE" | jq '.' >/dev/null 2>&1; then
112 |   # Extract the nested JSON from content[0].text and parse it
113 |   echo "$CALENDARS_RESPONSE" | jq -r '.result.content[0].text' | jq '.calendars[] | {id, summary, timeZone, accessRole}'
114 | elif echo "$CALENDARS_RESPONSE" | grep -q "^data:"; then
115 |   # SSE format - extract data, then nested content
116 |   echo "$CALENDARS_RESPONSE" | grep "^data:" | sed 's/^data: //' | jq -r '.result.content[0].text' | jq '.calendars[] | {id, summary, timeZone, accessRole}'
117 | else
118 |   echo "❌ List calendars failed - unknown format"
119 |   echo "$CALENDARS_RESPONSE"
120 | fi
121 | 
122 | # Test 5: Call list-colors tool
123 | echo -e "\n🎨 Testing list-colors tool..."
124 | 
125 | LIST_COLORS_REQUEST='{
126 |   "jsonrpc": "2.0",
127 |   "id": 4,
128 |   "method": "tools/call",
129 |   "params": {
130 |     "name": "list-colors",
131 |     "arguments": {}
132 |   }
133 | }'
134 | 
135 | COLORS_RESPONSE=$(curl -s -X POST "$SERVER_URL" \
136 |   -H "Content-Type: application/json" \
137 |   -H "Accept: application/json, text/event-stream" \
138 |   -H "mcp-session-id: $SESSION_ID" \
139 |   -d "$LIST_COLORS_REQUEST")
140 | 
141 | # Parse response appropriately
142 | if echo "$COLORS_RESPONSE" | jq '.' >/dev/null 2>&1; then
143 |   # Extract nested JSON and display color summary
144 |   echo "$COLORS_RESPONSE" | jq -r '.result.content[0].text' | jq '{
145 |     eventColors: .event | length,
146 |     calendarColors: .calendar | length,
147 |     sampleEventColor: .event["1"],
148 |     sampleCalendarColor: .calendar["1"]
149 |   }'
150 | elif echo "$COLORS_RESPONSE" | grep -q "^data:"; then
151 |   # SSE format
152 |   echo "$COLORS_RESPONSE" | grep "^data:" | sed 's/^data: //' | jq -r '.result.content[0].text' | jq '{
153 |     eventColors: .event | length,
154 |     calendarColors: .calendar | length,
155 |     sampleEventColor: .event["1"],
156 |     sampleCalendarColor: .calendar["1"]
157 |   }'
158 | else
159 |   echo "❌ List colors failed - unknown format"
160 |   echo "$COLORS_RESPONSE"
161 | fi
162 | 
163 | echo -e "\n✅ HTTP testing completed!"
164 | echo -e "\n💡 To test with different server URL: $0 http://your-server:port"
165 | 
```

--------------------------------------------------------------------------------
/docs/deployment.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Deployment Guide
  2 | 
  3 | This guide covers deploying the Google Calendar MCP Server for remote access via HTTP transport.
  4 | 
  5 | ## Transport Modes
  6 | 
  7 | ### stdio Transport (Default)
  8 | - Local use only
  9 | - Direct communication with Claude Desktop
 10 | - No network exposure
 11 | - Automatic authentication handling
 12 | 
 13 | ### HTTP Transport
 14 | - Remote deployment capable
 15 | - Server-Sent Events (SSE) for real-time communication
 16 | - Built-in security features
 17 | - Suitable for cloud deployment
 18 | 
 19 | ## HTTP Server Features
 20 | 
 21 | - ✅ **CORS Support**: Basic cross-origin access support
 22 | - ✅ **Health Monitoring**: Basic health check endpoint
 23 | - ✅ **Graceful Shutdown**: Proper resource cleanup
 24 | - ✅ **Origin Validation**: DNS rebinding protection
 25 | 
 26 | ## Local HTTP Deployment
 27 | 
 28 | ### Basic HTTP Server
 29 | 
 30 | ```bash
 31 | # Start on localhost only (default port 3000)
 32 | npm run start:http
 33 | ```
 34 | 
 35 | ### Public HTTP Server
 36 | 
 37 | ```bash
 38 | # Listen on all interfaces (0.0.0.0)
 39 | npm run start:http:public
 40 | ```
 41 | 
 42 | ### Environment Variables
 43 | 
 44 | ```bash
 45 | PORT=3000                    # Server port
 46 | HOST=localhost              # Bind address
 47 | TRANSPORT=http              # Transport mode
 48 | ```
 49 | 
 50 | ## Docker Deployment
 51 | 
 52 | ### Using Docker Compose (Recommended)
 53 | 
 54 | ```bash
 55 | # stdio mode  
 56 | docker compose up -d server
 57 | 
 58 | # HTTP mode
 59 | docker compose --profile http up -d
 60 | ```
 61 | 
 62 | See [Docker Guide](docker.md) for complete setup instructions.
 63 | 
 64 | ### Using Docker Run
 65 | 
 66 | ```bash
 67 | # Create volume for token storage
 68 | docker volume create mcp-tokens
 69 | 
 70 | # stdio mode
 71 | docker run -i \
 72 |   -v ./gcp-oauth.keys.json:/usr/src/app/gcp-oauth.keys.json:ro \
 73 |   -v mcp-tokens:/home/nodejs/.config/google-calendar-mcp \
 74 |   -e TRANSPORT=stdio \
 75 |   --name calendar-mcp \
 76 |   google-calendar-mcp
 77 | 
 78 | # HTTP mode
 79 | docker run -d \
 80 |   -p 3000:3000 \
 81 |   -v ./gcp-oauth.keys.json:/usr/src/app/gcp-oauth.keys.json:ro \
 82 |   -v mcp-tokens:/home/nodejs/.config/google-calendar-mcp \
 83 |   -e TRANSPORT=http \
 84 |   -e HOST=0.0.0.0 \
 85 |   --name calendar-mcp \
 86 |   google-calendar-mcp
 87 | 
 88 | ```
 89 | 
 90 | ### Building Custom Image
 91 | 
 92 | Use the provided Dockerfile which includes proper user setup and token storage:
 93 | 
 94 | ```bash
 95 | # Build image
 96 | docker build -t google-calendar-mcp .
 97 | 
 98 | # Run with authentication
 99 | docker run -it google-calendar-mcp npm run auth
100 | ```
101 | 
102 | ## Cloud Deployment
103 | 
104 | ### Google Cloud Run
105 | 
106 | ```bash
107 | # Build and push image
108 | gcloud builds submit --tag gcr.io/PROJECT-ID/calendar-mcp
109 | 
110 | # Deploy
111 | gcloud run deploy calendar-mcp \
112 |   --image gcr.io/PROJECT-ID/calendar-mcp \
113 |   --platform managed \
114 |   --region us-central1 \
115 |   --allow-unauthenticated \
116 |   --set-env-vars="TRANSPORT=http"
117 | ```
118 | 
119 | ### AWS ECS
120 | 
121 | 1. Push image to ECR
122 | 2. Create task definition with environment variables
123 | 3. Deploy service with ALB
124 | 
125 | ### Heroku
126 | 
127 | ```bash
128 | # Create app
129 | heroku create your-calendar-mcp
130 | 
131 | # Set buildpack
132 | heroku buildpacks:set heroku/nodejs
133 | 
134 | # Configure
135 | heroku config:set TRANSPORT=http
136 | heroku config:set GOOGLE_OAUTH_CREDENTIALS=./gcp-oauth.keys.json
137 | 
138 | # Deploy
139 | git push heroku main
140 | ```
141 | 
142 | ## Security Configuration
143 | 
144 | ### HTTPS/TLS
145 | 
146 | Always use HTTPS in production:
147 | 
148 | 1. **Behind a Reverse Proxy** (Recommended)
149 |    ```nginx
150 |    server {
151 |        listen 443 ssl;
152 |        server_name calendar-mcp.example.com;
153 |        
154 |        ssl_certificate /path/to/cert.pem;
155 |        ssl_certificate_key /path/to/key.pem;
156 |        
157 |        location / {
158 |            proxy_pass http://localhost:3000;
159 |            proxy_http_version 1.1;
160 |            proxy_set_header Upgrade $http_upgrade;
161 |            proxy_set_header Connection '';
162 |            proxy_set_header Host $host;
163 |            proxy_set_header X-Real-IP $remote_addr;
164 |        }
165 |    }
166 |    ```
167 | 
168 | 2. **Direct TLS** (Not built-in)
169 |    For TLS support, use a reverse proxy like nginx or a cloud load balancer with SSL termination.
170 | 
171 | 
172 | ### Authentication Flow
173 | 
174 | 1. Client connects to HTTP endpoint
175 | 2. Server redirects to Google OAuth
176 | 3. User authenticates with Google
177 | 4. Server stores tokens securely
178 | 5. Client receives session token
179 | 6. All requests use session token
180 | 
181 | ## Monitoring
182 | 
183 | ### Health Checks
184 | 
185 | ```bash
186 | # Health check
187 | curl http://localhost:3000/health
188 | ```
189 | 
190 | ### Logging
191 | 
192 | ```bash
193 | # Enable debug logging
194 | DEBUG=mcp:* npm run start:http
195 | 
196 | # JSON logging for production
197 | NODE_ENV=production npm run start:http
198 | ```
199 | 
200 | 
201 | ## Production Checklist
202 | 
203 | **OAuth App Setup:**
204 | - [ ] **Publish OAuth app to production in Google Cloud Console**
205 | - [ ] **Set up proper redirect URIs for your domain**
206 | - [ ] **Use production OAuth credentials (not test/development)**
207 | - [ ] **Consider submitting for verification to remove user warnings**
208 | 
209 | **Infrastructure:**
210 | - [ ] Use HTTPS/TLS encryption
211 | - [ ] Configure reverse proxy for production
212 | - [ ] Set up SSL termination
213 | - [ ] Set up monitoring/alerting for authentication failures
214 | - [ ] Configure log aggregation
215 | - [ ] Implement backup strategy for token storage
216 | - [ ] Test disaster recovery and re-authentication procedures
217 | - [ ] Review security headers
218 | - [ ] Enable graceful shutdown
219 | 
220 | **Note**: The 7-day token expiration is resolved by publishing your OAuth app to production in Google Cloud Console.
221 | 
222 | ## Troubleshooting
223 | 
224 | ### Connection Issues
225 | - Check firewall rules
226 | - Verify CORS configuration
227 | - Test with curl first
228 | 
229 | ### Authentication Failures
230 | - Ensure credentials are accessible
231 | - Check token permissions
232 | - Verify redirect URIs
233 | 
234 | ### Performance Problems
235 | - Enable caching headers
236 | - Use CDN for static assets
237 | - Monitor memory usage
238 | 
239 | See [Troubleshooting Guide](troubleshooting.md) for more solutions.
```
Page 1/6FirstPrevNextLast