#
tokens: 48652/50000 64/104 files (page 1/5)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 5. Use http://codebase.md/nspady/google-calendar-mcp?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
{
  ".": "2.0.6"
}

```

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

```
gcp-oauth.keys.json
.gcp-saved-tokens.json
```

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

```
.gcp-saved-tokens.json
gcp-oauth.keys.json

```

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

```
node_modules/
build/
*.log
gcp-oauth.keys.json
.gcp-saved-tokens.json
coverage/
.nyc_output/
coverage/
settings.local.json
.env

```

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

```
# Google Calendar MCP Server Configuration
# Transport mode
TRANSPORT=stdio  # Recommended for Claude Desktop integration

# HTTP settings (when TRANSPORT=http)
PORT=3000
HOST=0.0.0.0

# Development
DEBUG=false
NODE_ENV=production

# OAuth credentials path (required)
GOOGLE_OAUTH_CREDENTIALS=./gcp-oauth.keys.json

## Optional: Custom token storage location
# GOOGLE_CALENDAR_MCP_TOKEN_PATH=/custom/path/to/tokens

## OPTIONAL: Test Configuration (for development/testing only)
# [email protected]
# SEND_UPDATES=none
# AUTO_CLEANUP=true

## OPTIONAL: Anthropic API config (used only for integration testing)
# ANTHROPIC_MODEL=claude-3-5-haiku-20241022
# CLAUDE_API_KEY={your_api_key}

# OPTIONAL: Open AI API config (used only for integration testing)
# OPENAI_API_KEY={your_api_key}
# OPENAI_MODEL=gpt-4.1
```

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

```markdown
# Documentation Index

Welcome to the Google Calendar MCP Server documentation.

## Getting Started

- [Main README](../README.md) - Quick start guide and overview
- [Authentication Setup](authentication.md) - Detailed Google Cloud setup instructions

## User Guides

- [Advanced Usage](advanced-usage.md) - Multi-account, batch operations, smart scheduling
- [Troubleshooting](troubleshooting.md) - Common issues and solutions

## Deployment

- [Deployment Guide](deployment.md) - HTTP transport, Docker, cloud deployment

## Development

- [Development Guide](development.md) - Contributing and development setup
- [Architecture Overview](architecture.md) - Technical architecture details
- [Testing Guide](testing.md) - Running and writing tests
- [Development Scripts](development-scripts.md) - Using the dev command system

## Reference

- [API Documentation](https://developers.google.com/calendar/api/v3/reference) - Google Calendar API
- [MCP Specification](https://modelcontextprotocol.io/docs) - Model Context Protocol

## Quick Links

### For Users
1. Start with the [Main README](../README.md)
2. Follow [Authentication Setup](authentication.md)
3. Check [Troubleshooting](troubleshooting.md) if needed

### For Developers
1. Read [Architecture Overview](architecture.md)
2. Set up with [Development Guide](development.md)
3. Run tests with [Testing Guide](testing.md)

### For Deployment
1. Review [Deployment Guide](deployment.md)
2. Check security considerations
3. Set up monitoring

## Need Help?

- [GitHub Issues](https://github.com/nspady/google-calendar-mcp/issues)
- [GitHub Discussions](https://github.com/nspady/google-calendar-mcp/discussions)
```

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

```markdown
# GitHub Actions Workflows

This directory contains automated CI/CD workflows for the Google Calendar MCP project.

## Workflows

### 1. `schema-validation.yml` - Schema Validation and Tests
**Triggers**: Push/PR to main or develop branches

Simple workflow focused on schema validation and basic testing:
- Builds the project
- Validates MCP schemas for compatibility
- Runs schema-specific tests
- Runs unit tests via `npm run dev test`

### 2. `ci.yml` - Comprehensive CI Pipeline
**Triggers**: Push/PR to main, develop, or feature branches

Full CI pipeline with multiple jobs running in parallel/sequence:

#### Jobs:
1. **code-quality**: 
   - Checks for console.log statements
   - Ensures code follows best practices

2. **build-and-validate**:
   - Builds the project
   - Validates MCP schemas
   - Uploads build artifacts

3. **unit-tests**:
   - Runs on multiple Node.js versions (18, 20)
   - Executes all unit tests
   - Runs schema compatibility tests

4. **integration-tests** (optional):
   - Only runs on main branch or PRs
   - Executes direct integration tests
   - Continues on error to not block CI

5. **coverage**:
   - Generates test coverage reports
   - Uploads coverage artifacts

## Running Locally

To test workflows locally before pushing:

```bash
# Run schema validation
npm run dev validate-schemas

# Run dev tests (unit tests only)
npm run dev test

# Run all tests
npm test

# Run with coverage
npm run dev coverage
```

## Environment Variables

All workflows set `NODE_ENV=test` to:
- Use test account credentials
- Skip authentication prompts
- Enable test-specific behavior

## Best Practices

1. **Always run `npm run dev test` before pushing** - catches most issues quickly
2. **Schema changes** - Run `npm run validate-schemas` to ensure compatibility
3. **Console statements** - Use `process.stderr.write()` instead of `console.log()`
4. **Integration tests** - These may fail in CI due to API limits; that's OK

## Troubleshooting

### Schema validation fails
- Check for `oneOf`, `anyOf`, `allOf` in tool schemas
- Ensure datetime fields have proper format and timezone info
- Run `npm run dev validate-schemas` locally

### Unit tests fail
- Run `npm run dev test` locally
- Check for recent schema changes that might affect tests
- Ensure all console.log statements are removed

### Integration tests fail
- These are marked as `continue-on-error` in CI
- Usually due to API rate limits or authentication issues
- Can be ignored if unit tests pass

## Adding New Workflows

When adding new workflows:
1. Test locally first
2. Use matrix builds for multiple versions
3. Set appropriate environment variables
4. Consider job dependencies and parallelization
5. Add documentation here
```

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

```markdown
# Google Calendar MCP Server

A Model Context Protocol (MCP) server that provides Google Calendar integration for AI assistants like Claude.

## Features

- **Multi-Calendar Support**: List events from multiple calendars simultaneously
- **Event Management**: Create, update, delete, and search calendar events
- **Recurring Events**: Advanced modification capabilities for recurring events
- **Free/Busy Queries**: Check availability across calendars
- **Smart Scheduling**: Natural language understanding for dates and times
- **Inteligent Import**: Add calendar events from images, PDFs or web links

## Quick Start

### Prerequisites

1. A Google Cloud project with the Calendar API enabled
2. OAuth 2.0 credentials (Desktop app type)

### Google Cloud Setup

1. Go to the [Google Cloud Console](https://console.cloud.google.com)
2. Create a new project or select an existing one.
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.
4. Create OAuth 2.0 credentials:
   - Go to Credentials
   - Click "Create Credentials" > "OAuth client ID"
   - Choose "User data" for the type of data that the app will be accessing
   - Add your app name and contact information
   - Add the following scopes (optional):
     - `https://www.googleapis.com/auth/calendar.events` and `https://www.googleapis.com/auth/calendar`
   - Select "Desktop app" as the application type (Important!)
   - Save the auth key, you'll need to add its path to the JSON in the next step
   - Add your email address as a test user under the [Audience screen](https://console.cloud.google.com/auth/audience)
      - 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.
      - 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).

### Installation

**Option 1: Use with npx (Recommended)**

Add to your Claude Desktop configuration:

**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
  "mcpServers": {
    "google-calendar": {
      "command": "npx",
      "args": ["@cocal/google-calendar-mcp"],
      "env": {
        "GOOGLE_OAUTH_CREDENTIALS": "/path/to/your/gcp-oauth.keys.json"
      }
    }
  }
}
```

**⚠️ Important Note for npx Users**: When using npx, you **must** specify the credentials file path using the `GOOGLE_OAUTH_CREDENTIALS` environment variable.

**Option 2: Local Installation**

```bash
git clone https://github.com/nspady/google-calendar-mcp.git
cd google-calendar-mcp
npm install
npm run build
```

Then add to Claude Desktop config using the local path or by specifying the path with the `GOOGLE_OAUTH_CREDENTIALS` environment variable.

**Option 3: Docker Installation**

```bash
git clone https://github.com/nspady/google-calendar-mcp.git
cd google-calendar-mcp
cp /path/to/your/gcp-oauth.keys.json .
docker compose up
```

See the [Docker deployment guide](docs/docker.md) for detailed configuration options including HTTP transport mode.

### First Run

1. Start Claude Desktop
2. The server will prompt for authentication on first use
3. Complete the OAuth flow in your browser
4. You're ready to use calendar features!

### Re-authentication

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:

**For npx users:**
```bash
export GOOGLE_OAUTH_CREDENTIALS="/path/to/your/gcp-oauth.keys.json"
npx @cocal/google-calendar-mcp auth
```

**For local installation:**
```bash
npm run auth
```

**To avoid weekly re-authentication**, publish your app to production mode (without verification):
1. Go to Google Cloud Console → "APIs & Services" → "OAuth consent screen"
2. Click "PUBLISH APP" and confirm
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. 

See [Authentication Guide](docs/authentication.md#moving-to-production-mode-recommended) for details.

## Example Usage

Along with the normal capabilities you would expect for a calendar integration you can also do really dynamic, multi-step processes like:

1. **Cross-calendar availability**:
   ```
   Please provide availability looking at both my personal and work calendar for this upcoming week.
   I am looking for a good time to meet with someone in London for 1 hr.
   ```

2. Add events from screenshots, images and other data sources:
   ```
   Add this event to my calendar based on the attached screenshot.
   ```
   Supported image formats: PNG, JPEG, GIF
   Images can contain event details like date, time, location, and description

3. Calendar analysis:
   ```
   What events do I have coming up this week that aren't part of my usual routine?
   ```
4. Check attendance:
   ```
   Which events tomorrow have attendees who have not accepted the invitation?
   ```
5. Auto coordinate events:
   ```
   Here's some available that was provided to me by someone. {available times}
   Take a look at the times provided and let me know which ones are open on my calendar.
   ```

## Available Tools

| Tool | Description |
|------|-------------|
| `list-calendars` | List all available calendars |
| `list-events` | List events with date filtering |
| `search-events` | Search events by text query |
| `create-event` | Create new calendar events |
| `update-event` | Update existing events |
| `delete-event` | Delete events |
| `get-freebusy` | Check availability across calendars, including external calendars |
| `list-colors` | List available event colors |

## Documentation

- [Authentication Setup](docs/authentication.md) - Detailed Google Cloud setup
- [Advanced Usage](docs/advanced-usage.md) - Multi-account, batch operations
- [Deployment Guide](docs/deployment.md) - HTTP transport, remote access
- [Docker Guide](docs/docker.md) - Docker deployment with stdio and HTTP modes
- [OAuth Verification](docs/oauth-verification.md) - Moving from test to production mode
- [Architecture](docs/architecture.md) - Technical architecture overview
- [Development](docs/development.md) - Contributing and testing
- [Testing](docs/testing.md) - Unit and integration testing guide

## Configuration

**Environment Variables:**
- `GOOGLE_OAUTH_CREDENTIALS` - Path to OAuth credentials file
- `GOOGLE_CALENDAR_MCP_TOKEN_PATH` - Custom token storage location (optional)

**Claude Desktop Config Location:**
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`


## Security

- OAuth tokens are stored securely in your system's config directory
- Credentials never leave your local machine
- All calendar operations require explicit user consent

### Troubleshooting

1. **OAuth Credentials File Not Found:**
   - For npx users: You **must** specify the credentials file path using `GOOGLE_OAUTH_CREDENTIALS`
   - Verify file paths are absolute and accessible

2. **Authentication Errors:**
   - Ensure your credentials file contains credentials for a **Desktop App** type
   - Verify your user email is added as a **Test User** in the Google Cloud OAuth Consent screen
   - Try deleting saved tokens and re-authenticating
   - Check that no other process is blocking ports 3000-3004

3. **Build Errors:**
   - Run `npm install && npm run build` again
   - Check Node.js version (use LTS)
   - Delete the `build/` directory and run `npm run build`
4. **"Something went wrong" screen during browser authentication**
   - Perform manual authentication per the below steps
   - Use a Chromium-based browser to open the authentication URL. Test app authentication may not be supported on some non-Chromium browsers.

5. **"User Rate Limit Exceeded" errors**
   - This typically occurs when your OAuth credentials are missing project information
   - Ensure your `gcp-oauth.keys.json` file includes `project_id`
   - Re-download credentials from Google Cloud Console if needed
   - The file should have format: `{"installed": {"project_id": "your-project-id", ...}}`

### Manual Authentication
For re-authentication or troubleshooting:
```bash
# For npx installations
export GOOGLE_OAUTH_CREDENTIALS="/path/to/your/credentials.json"
npx @cocal/google-calendar-mcp auth

# For local installations
npm run auth
```

## License

MIT

## Support

- [GitHub Issues](https://github.com/nspady/google-calendar-mcp/issues)
- [Documentation](docs/)

```

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

```markdown
# Repository Guidelines

## Project Structure & Modules
- Source: `src/` (entry `index.ts`), builds to `build/` via esbuild.
- Handlers: `src/handlers/core/` (tool implementations), utilities in `src/handlers/utils/`.
- Schemas: `src/schemas/` (Zod definitions shared between server and tests).
- Services: `src/services/` (conflict detection, helpers), transports in `src/transports/`.
- Auth: `src/auth/`, OAuth helper `src/auth-server.ts`.
- Tests: `src/tests/unit/` and `src/tests/integration/`.
- Docs: `docs/` (auth, testing, deployment, architecture).

## Build, Test, and Dev
- `npm run build`: Bundle to `build/index.js` and `build/auth-server.js` (Node 18 ESM).
- `npm start`: Run stdio transport (for Claude Desktop). Example: `npx @cocal/google-calendar-mcp`.
- `npm run start:http`: HTTP transport on `:3000` (use `start:http:public` for `0.0.0.0`).
- `npm test`: Vitest unit tests. `npm run test:integration` for Google/LLM integration.
- `npm run dev`: Helper menu (auth, http, docker, targeted test runs).
- `npm run auth`: Launch local OAuth flow (stores tokens in `~/.config/google-calendar-mcp`).

## Coding Style & Naming
- TypeScript, strict typing (avoid `any`). 2‑space indentation.
- Files: PascalCase for handlers/services (e.g., `GetEventHandler.ts`), camelCase for functions/vars.
- ESM modules with `type: module`; prefer named exports.
- Validation with Zod in `src/schemas/`; validate inputs at handler boundaries.
- Linting: `npm run lint` (TypeScript no‑emit checks).

## Testing Guidelines
- Framework: Vitest with V8 coverage (`npm run test:coverage`).
- Unit test names: `*.test.ts` mirroring source paths (e.g., `src/tests/unit/handlers/...`).
- Integration requires env: `GOOGLE_OAUTH_CREDENTIALS`, `TEST_CALENDAR_ID`; authenticate with `npm run dev auth:test`.
- Use `src/tests/integration/test-data-factory.ts` utilities; ensure tests clean up created events.

## Commit & PRs
- Commits: Imperative mood, concise subject, optional scope. Examples:
  - `Fix timezone handling for list-events`
  - `services(conflict): improve duplicate detection`
- Reference issues/PRs with `(#NN)` when applicable.
- PRs: clear description, rationale, screenshots/log snippets when debugging; link issues; list notable env/config changes.
- Required before PR: `npm run lint && npm test && npm run build` (and relevant integration tests if affected).

## Security & Config
- Keep credentials out of git; use `.env` and `GOOGLE_OAUTH_CREDENTIALS` path.
- Test vs normal accounts controlled via `GOOGLE_ACCOUNT_MODE`; prefer `test` for integration.
- Tokens stored locally in `~/.config/google-calendar-mcp/tokens.json`.

## Adding New Tools (MCP)
- Implement handler in `src/handlers/core/YourToolHandler.ts` extending `BaseToolHandler`.
- Define/extend Zod schema in `src/schemas/` and add unit + integration tests.
- Handlers are auto‑registered; update docs if adding public tool names.


```

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

```markdown
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

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.

## Development Commands

```bash
npm install              # Install dependencies
npm run build            # Build with esbuild (outputs to build/)
npm run dev              # Show interactive development menu with all commands
npm run lint             # TypeScript type checking (no emit)

# Testing - Quick Start
npm test                             # Unit tests only (no auth required)
npm run test:watch                   # Unit tests in watch mode
npm run dev test:integration:direct  # Direct integration tests (recommended for dev)
npm run dev coverage                 # Generate test coverage report

# Testing - Full Suite (rarely needed, incurrs LLM usage costs)
npm run dev test:integration:claude  # Claude + MCP integration (requires CLAUDE_API_KEY)
npm run dev test:integration:openai  # OpenAI + MCP integration (requires OPENAI_API_KEY)
npm run dev test:integration:all     # All integration tests (requires all API keys)

# Authentication
npm run auth                # Authenticate main account
npm run dev auth:test       # Authenticate test account (for integration tests)
npm run dev account:status  # Check authentication status

# Running the server
npm start                        # Start with stdio transport
npm run dev http                 # Start HTTP server on localhost:3000
npm run dev http:public          # HTTP server accessible from any host
```

## Architecture

### Handler Architecture

All MCP tools follow a consistent handler pattern:

1. **Handler Registration**: Handlers are auto-registered via `src/tools/registry.ts`
2. **Base Class**: All handlers extend `BaseToolHandler` from `src/handlers/core/BaseToolHandler.ts`
3. **Schema Definition**: Input schemas defined in `src/tools/registry.ts` using Zod
4. **Handler Implementation**: Core logic in `src/handlers/core/` directory

**Request Flow:**
```
Client → Transport Layer → Schema Validation (Zod) → Handler → Google Calendar API → Response
```

### Adding New Tools

1. Create handler class in `src/handlers/core/YourToolHandler.ts`:
   - Extend `BaseToolHandler`
   - Implement `runTool(args, oauth2Client)` method
   - Use `this.getCalendar(oauth2Client)` to get Calendar API client
   - Use `this.handleGoogleApiError(error)` for error handling

2. Define schema in `src/tools/registry.ts`:
   - Add to `ToolSchemas` object with Zod schema
   - Add to `ToolRegistry.tools` array with name, description, handler class

3. Add tests:
   - Unit tests in `src/tests/unit/handlers/YourToolHandler.test.ts`
   - Integration tests in `src/tests/integration/` if needed

**No manual registration needed** - handlers are auto-discovered by the registry system.

### Authentication System

- **OAuth 2.0** with refresh token support
- **Multi-account**: Supports `normal` (default) and `test` accounts via `GOOGLE_ACCOUNT_MODE` env var
- **Token Storage**: `~/.config/google-calendar-mcp/tokens.json` (platform-specific paths)
- **Token Validation**: Automatic refresh on expiry
- **Components**:
  - `src/auth/client.ts` - OAuth2Client initialization
  - `src/auth/server.ts` - Auth server for OAuth flow
  - `src/auth/tokenManager.ts` - Token management and validation

### Transport Layer

- **stdio** (default): Process communication for Claude Desktop
- **HTTP**: RESTful API with SSE for remote deployment
- **Configuration**: `src/config/TransportConfig.ts`
- **Handlers**: `src/transports/stdio.ts` and `src/transports/http.ts`

### Testing Strategy

**Unit Tests** (`src/tests/unit/`):
- No external dependencies (mocked)
- Schema validation, error handling, datetime logic
- Run with `npm test` (no setup required)

**Integration Tests** (`src/tests/integration/`):

Three types of integration tests, each with different requirements:

1. **Direct Integration** (most commonly used):
   - File: `direct-integration.test.ts`
   - Tests real Google Calendar API calls
   - **Setup Required**:
     ```bash
     # 1. Set credentials path
     export GOOGLE_OAUTH_CREDENTIALS=./gcp-oauth.keys.json

     # 2. Set test calendar (use "primary" or a specific calendar ID)
     export TEST_CALENDAR_ID=primary

     # 3. Authenticate test account
     npm run dev auth:test

     # 4. Run tests
     npm run dev test:integration:direct
     ```

2. **LLM Integration** (rarely needed):
   - Files: `claude-mcp-integration.test.ts`, `openai-mcp-integration.test.ts`
   - Tests end-to-end MCP protocol with AI models
   - **Additional Setup** (beyond direct integration setup):
     ```bash
     # For Claude tests
     export CLAUDE_API_KEY=sk-ant-...
     npm run dev test:integration:claude

     # For OpenAI tests
     export OPENAI_API_KEY=sk-...
     npm run dev test:integration:openai

     # For both
     npm run dev test:integration:all
     ```
   - ⚠️ Consumes API credits and takes 2-5 minutes

**Quick Setup Summary:**
```bash
# Minimal setup for development (direct integration tests only):
export GOOGLE_OAUTH_CREDENTIALS=./gcp-oauth.keys.json
export TEST_CALENDAR_ID=primary
npm run dev auth:test
npm run dev test:integration:direct
```

### Key Services

**Conflict Detection** (`src/services/conflict-detection/`):
- `ConflictAnalyzer.ts` - Detects scheduling conflicts
- `EventSimilarityChecker.ts` - Identifies duplicate events
- `ConflictDetectionService.ts` - Main service coordinating conflict checks
- Used by `create-event` and `update-event` handlers

**Structured Responses** (`src/types/structured-responses.ts`):
- TypeScript interfaces for consistent response formats
- Used across handlers for type safety

**Utilities**:
- `src/utils/field-mask-builder.ts` - Builds Google API field masks
- `src/utils/event-id-validator.ts` - Validates Google Calendar event IDs
- `src/utils/response-builder.ts` - Formats MCP responses
- `src/handlers/utils/datetime.ts` - Timezone and datetime utilities

## Important Patterns

### Timezone Handling

- **Preferred Format**: ISO 8601 without timezone (e.g., `2024-01-01T10:00:00`)
  - Uses `timeZone` parameter or calendar's default timezone
- **Also Supported**: ISO 8601 with timezone (e.g., `2024-01-01T10:00:00-08:00`)
- **All-day Events**: Date only format (e.g., `2024-01-01`)
- **Helper**: `getCalendarTimezone()` method in `BaseToolHandler`

### Multi-Calendar Support

- `list-events` accepts single calendar ID or JSON array: `'["cal1", "cal2"]'`
- Batch requests handled by `BatchRequestHandler.ts`
- Maximum 50 calendars per request

### Recurring Events

- Modification scopes: `thisEventOnly`, `thisAndFollowing`, `all`
- Handled by `RecurringEventHelpers.ts`
- Special validation in `update-event` schema

### Error Handling

- Use `McpError` from `@modelcontextprotocol/sdk/types.js`
- `BaseToolHandler.handleGoogleApiError()` for consistent Google API error handling
- Maps HTTP status codes to appropriate MCP error codes

### Structured Output Migration

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.

### MCP Structure

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.

## Code Quality

- **TypeScript**: Strict mode, avoid `any` types
- **Formatting**: Use existing patterns in handlers
- **Testing**: Add unit tests for all new handlers
- **Error Messages**: Clear, actionable error messages referencing Google Calendar concepts

## Google Calendar API

- **Version**: v3 (`googleapis` package)
- **Timeout**: 3 seconds per API call (configured in `BaseToolHandler`)
- **Rate Limiting**: Google Calendar API has quotas - integration tests may hit limits
- **Scopes Required**:
  - `https://www.googleapis.com/auth/calendar.events`
  - `https://www.googleapis.com/auth/calendar`

## Deployment

- **npx**: `npx @cocal/google-calendar-mcp` (requires `GOOGLE_OAUTH_CREDENTIALS` env var)
- **Docker**: See `docs/docker.md` for Docker deployment with stdio and HTTP modes
- **Claude Desktop Config**: See README.md for local stdio configuration

### Deployment Modes

**Local Development (Claude Desktop):**
- Use **stdio mode** (default)
- No server or domain required
- Direct process communication
- See README.md for setup

**Key Differences:**
- **stdio**: For Claude Desktop only, local machine
- **HTTP**: For testing, development, debugging (local only)


```

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

```typescript
export function getSecureTokenPath(): string;
export function getLegacyTokenPath(): string;
export function getAccountMode(): 'normal' | 'test';


```

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

```json
{
  "extends": "./tsconfig.json",
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "src/tests/**"
  ],
  "compilerOptions": {
    "noEmit": true,
    "allowJs": true
  }
}


```

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

```json
{
    "installed": {
        "client_id": "YOUR_GOOGLE_CLIENT_ID",
        "client_secret": "YOUR_GOOGLE_CLIENT_SECRET",
        "redirect_uris": ["http://localhost:3000/oauth2callback"]
    }
}

```

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

```typescript
export { ConflictDetectionService } from './ConflictDetectionService.js';
export { EventSimilarityChecker } from './EventSimilarityChecker.js';
export { ConflictAnalyzer } from './ConflictAnalyzer.js';
export * from './types.js';
```

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

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "types": ["node"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

```

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

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

export class StdioTransportHandler {
  private server: McpServer;

  constructor(server: McpServer) {
    this.server = server;
  }

  async connect(): Promise<void> {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
  }
} 
```

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

```typescript
/**
 * Centralized configuration for conflict detection thresholds
 */

export const CONFLICT_DETECTION_CONFIG = {
  /**
   * Thresholds for duplicate event detection
   */
  DUPLICATE_THRESHOLDS: {
    /**
     * Events with similarity >= this value are flagged as potential duplicates
     * and shown as warnings during creation
     */
    WARNING: 0.7,
    
    /**
     * Events with similarity >= this value are considered exact duplicates
     * and block creation unless explicitly overridden with allowDuplicates flag
     */
    BLOCKING: 0.95
  },
  
  /**
   * Default similarity threshold for duplicate detection
   * Used when duplicateSimilarityThreshold is not specified in the request
   */
  DEFAULT_DUPLICATE_THRESHOLD: 0.7
} as const;

export type ConflictDetectionConfig = typeof CONFLICT_DETECTION_CONFIG;
```

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

```typescript
import { defineConfig } from 'vitest/config'
import { loadEnv } from 'vite'

export default defineConfig({
  test: {
    globals: true, // Use Vitest globals (describe, it, expect) like Jest
    environment: 'node', // Specify the test environment
    // Load environment variables from .env file
    env: loadEnv('', process.cwd(), ''),
    // Increase timeout for AI API calls
    testTimeout: 30000,
    include: [
      'src/tests/**/*.test.ts'
    ],
    // Exclude integration tests by default (they require credentials)
    exclude: ['**/node_modules/**'],
    // Enable coverage
    coverage: {
      provider: 'v8', // or 'istanbul'
      reporter: ['text', 'json', 'html'],
      exclude: [
        '**/node_modules/**',
        'src/tests/integration/**',
        'build/**',
        'scripts/**',
        '*.config.*'
      ],
    },
  },
}) 
```

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

```markdown
# Development Guide

## Setup

```bash
git clone https://github.com/nspady/google-calendar-mcp.git
cd google-calendar-mcp
npm install
npm run build
npm run auth                # Authenticate main account
npm run dev auth:test       # Authenticate test account (used for integration tests) 
```

## Development

```bash
npm run dev         # Interactive development menu
npm run build       # Build project  
npm run lint        # Type-check with TypeScript (no emit)
npm test            # Run tests
```

## Contributing

- Follow existing code patterns
- Add tests for new features  
- Use TypeScript strictly (avoid `any`)
- Run `npm run dev` for development tools

## Adding New Tools

1. Create handler in `src/handlers/core/NewToolHandler.ts`
2. Define schema in `src/schemas/`  
3. Add tests in `src/tests/`
4. Auto-discovered by registry system

See existing handlers for patterns.

```

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

```dockerfile
# Google Calendar MCP Server - Optimized Dockerfile
# syntax=docker/dockerfile:1

FROM node:18-alpine

# Create app user for security
RUN addgroup -g 1001 -S nodejs && \
    adduser -S -u 1001 -G nodejs nodejs

# Set working directory
WORKDIR /app

# Copy package files for dependency caching
COPY package*.json ./

# Copy build scripts and source files needed for build
COPY scripts ./scripts
COPY src ./src
COPY tsconfig.json .

# Install all dependencies (including dev dependencies for build)
RUN npm ci --no-audit --no-fund --silent

# Build the project
RUN npm run build

# Remove dev dependencies to reduce image size
RUN npm prune --production --silent

# Create config directory and set permissions
RUN mkdir -p /home/nodejs/.config/google-calendar-mcp && \
    chown -R nodejs:nodejs /home/nodejs/.config && \
    chown -R nodejs:nodejs /app

# Switch to non-root user
USER nodejs

# Expose port for HTTP mode (optional)
EXPOSE 3000

# Default command - run directly to avoid npm output
CMD ["node", "build/index.js"]
```

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

```javascript
#!/usr/bin/env node

/**
 * Shared path utilities for token management
 * This module provides consistent token path resolution across all scripts
 */

import path from 'path';
import { homedir } from 'os';

/**
 * Get the secure token storage path
 * Uses XDG Base Directory specification on Unix-like systems
 */
export function getSecureTokenPath() {
  const configDir = process.env.XDG_CONFIG_HOME || path.join(homedir(), '.config');
  return path.join(configDir, 'google-calendar-mcp', 'tokens.json');
}

/**
 * Get the legacy token path (for migration purposes)
 */
export function getLegacyTokenPath() {
  return path.join(process.cwd(), '.gcp-saved-tokens.json');
}

/**
 * Get current account mode from environment
 * Uses same logic as utils.ts but compatible with both JS and TS
 */
export function getAccountMode() {
  // If set explicitly via environment variable use that instead
  const explicitMode = process.env.GOOGLE_ACCOUNT_MODE?.toLowerCase();
  if (explicitMode === 'test' || explicitMode === 'normal') {
    return explicitMode;
  }
  
  // Auto-detect test environment
  if (process.env.NODE_ENV === 'test') {
    return 'test';
  }
  
  // Default to normal for regular app usage
  return 'normal';
}
```

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

```typescript
// TypeScript interfaces for Google Calendar data structures

export interface CalendarListEntry {
  id?: string | null;
  summary?: string | null;
}

export interface CalendarEventReminder {
  method: 'email' | 'popup';
  minutes: number;
}

export interface CalendarEventAttendee {
  email?: string | null;
  responseStatus?: string | null;
}

export interface CalendarEvent {
  id?: string | null;
  summary?: string | null;
  start?: {
    dateTime?: string | null;
    date?: string | null;
    timeZone?: string | null;
  };
  end?: {
    dateTime?: string | null;
    date?: string | null;
    timeZone?: string | null;
  };
  location?: string | null;
  attendees?: CalendarEventAttendee[] | null;
  colorId?: string | null;
  reminders?: {
    useDefault: boolean;
    overrides?: CalendarEventReminder[];
  };
  recurrence?: string[] | null;
}

// Type-safe response based on Google Calendar FreeBusy API
export interface FreeBusyResponse {
  kind: "calendar#freeBusy";
  timeMin: string;
  timeMax: string;
  groups?: {
    [key: string]: {
      errors?: { domain: string; reason: string }[];
      calendars?: string[];
    };
  };
  calendars: {
    [key: string]: {
      errors?: { domain: string; reason: string }[];
      busy: {
        start: string;
        end: string;
      }[];
    };
  };
}
```

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

```typescript
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { OAuth2Client } from "google-auth-library";
import { BaseToolHandler } from "./BaseToolHandler.js";
import { DeleteEventInput } from "../../tools/registry.js";
import { DeleteEventResponse } from "../../types/structured-responses.js";
import { createStructuredResponse } from "../../utils/response-builder.js";

export class DeleteEventHandler extends BaseToolHandler {
    async runTool(args: any, oauth2Client: OAuth2Client): Promise<CallToolResult> {
        const validArgs = args as DeleteEventInput;
        await this.deleteEvent(oauth2Client, validArgs);

        const response: DeleteEventResponse = {
            success: true,
            eventId: validArgs.eventId,
            calendarId: validArgs.calendarId,
            message: "Event deleted successfully"
        };

        return createStructuredResponse(response);
    }

    private async deleteEvent(
        client: OAuth2Client,
        args: DeleteEventInput
    ): Promise<void> {
        try {
            const calendar = this.getCalendar(client);
            await calendar.events.delete({
                calendarId: args.calendarId,
                eventId: args.eventId,
                sendUpdates: args.sendUpdates,
            });
        } catch (error) {
            throw this.handleGoogleApiError(error);
        }
    }
}

```

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

```yaml
# Google Calendar MCP Server - Docker Compose Configuration
# Simple, production-ready setup following Docker best practices

services:
  calendar-mcp:
    build: .
    container_name: calendar-mcp
    restart: unless-stopped
    
    # Environment configuration via .env file
    env_file: .env

    # OAuth credentials and token storage
    volumes:
      - ./gcp-oauth.keys.json:/app/gcp-oauth.keys.json:ro
      - calendar-tokens:/home/nodejs/.config/google-calendar-mcp
    
    # Expose ports for HTTP mode and OAuth authentication
    ports:
      - "3000:3000"  # HTTP mode MCP server
      - "3500:3500"  # OAuth authentication
      - "3501:3501"
      - "3502:3502"
      - "3503:3503"
      - "3504:3504"
      - "3505:3505"
    
    
    # Resource limits for production stability
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"
        reservations:
          memory: 256M
          cpus: "0.5"
    
    # Security options
    security_opt:
      - no-new-privileges:true
    
    # Health check for HTTP mode (safe for stdio mode too)
    healthcheck:
      test: ["CMD-SHELL", "if [ \"$TRANSPORT\" = \"http\" ]; then curl -f http://localhost:${PORT:-3000}/health || exit 1; else exit 0; fi"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

# Persistent volume for OAuth tokens
volumes:
  calendar-tokens:
    driver: local
```

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

```typescript
import { calendar_v3 } from "googleapis";

/**
 * Internal conflict info used by the conflict detection service.
 * Contains additional internal fields not exposed in public API responses.
 */
export interface InternalConflictInfo {
  type: 'overlap' | 'duplicate';
  calendar: string;
  event: {
    id: string;
    title: string;
    url?: string;
    start?: string;
    end?: string;
  };
  fullEvent?: calendar_v3.Schema$Event;
  overlap?: {
    duration: string;
    percentage: number;
    startTime: string;
    endTime: string;
  };
  similarity?: number;
}

/**
 * Internal duplicate info used by the conflict detection service.
 * Contains additional internal fields not exposed in public API responses.
 */
export interface InternalDuplicateInfo {
  event: {
    id: string;
    title: string;
    start?: string;
    end?: string;
    url?: string;
    similarity: number;
  };
  fullEvent?: calendar_v3.Schema$Event;
  calendarId?: string;
  suggestion: string;
}

export interface ConflictCheckResult {
  hasConflicts: boolean;
  conflicts: InternalConflictInfo[];
  duplicates: InternalDuplicateInfo[];
}

export interface EventTimeRange {
  start: Date;
  end: Date;
  isAllDay: boolean;
}

export interface ConflictDetectionOptions {
  checkDuplicates?: boolean;
  checkConflicts?: boolean;
  calendarsToCheck?: string[];
  duplicateSimilarityThreshold?: number;
  includeDeclinedEvents?: boolean;
}
```

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

```yaml
name: Release

on:
  push:
    branches:
      - main

permissions:
  contents: write
  pull-requests: write
  id-token: write

jobs:
  release-please:
    runs-on: ubuntu-latest
    steps:
      - uses: googleapis/release-please-action@v4
        id: release
        with:
          release-type: node

      # Only run the following steps if a release was created
      - uses: actions/checkout@v4
        if: ${{ steps.release.outputs.release_created }}

      - uses: actions/setup-node@v4
        if: ${{ steps.release.outputs.release_created }}
        with:
          node-version: '18'
          registry-url: 'https://registry.npmjs.org'

      - name: Install dependencies
        if: ${{ steps.release.outputs.release_created }}
        run: npm ci

      - name: Build project
        if: ${{ steps.release.outputs.release_created }}
        run: npm run build

      - name: Publish to NPM
        if: ${{ steps.release.outputs.release_created }}
        run: |
          VERSION="${{ steps.release.outputs.tag_name }}"

          # Check if this is a prerelease version (contains -, like v1.3.0-beta.0)
          if [[ "$VERSION" == *"-"* ]]; then
            echo "Publishing prerelease version $VERSION with beta tag"
            npm publish --provenance --access public --tag beta
          else
            echo "Publishing stable version $VERSION with latest tag"
            npm publish --provenance --access public
          fi
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

```

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

```javascript
#!/usr/bin/env node

import * as esbuild from 'esbuild';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __dirname = dirname(fileURLToPath(import.meta.url));
const isWatch = process.argv.includes('--watch');

/** @type {import('esbuild').BuildOptions} */
const buildOptions = {
  entryPoints: [join(__dirname, '../src/index.ts')],
  bundle: true,
  platform: 'node',
  target: 'node18',
  outfile: join(__dirname, '../build/index.js'),
  format: 'esm',
  banner: {
    js: '#!/usr/bin/env node\n',
  },
  packages: 'external', // Don't bundle node_modules
  sourcemap: true,
};

/** @type {import('esbuild').BuildOptions} */
const authServerBuildOptions = {
  entryPoints: [join(__dirname, '../src/auth-server.ts')],
  bundle: true,
  platform: 'node',
  target: 'node18',
  outfile: join(__dirname, '../build/auth-server.js'),
  format: 'esm',
  packages: 'external', // Don't bundle node_modules
  sourcemap: true,
};

if (isWatch) {
  const context = await esbuild.context(buildOptions);
  const authContext = await esbuild.context(authServerBuildOptions);
  await Promise.all([context.watch(), authContext.watch()]);
  process.stderr.write('Watching for changes...\n');
} else {
  await Promise.all([
    esbuild.build(buildOptions),
    esbuild.build(authServerBuildOptions)
  ]);
  
  // Make the file executable on non-Windows platforms
  if (process.platform !== 'win32') {
    const { chmod } = await import('fs/promises');
    await chmod(buildOptions.outfile, 0o755);
  }
} 
```

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

```markdown
# Architecture Overview

## Transport Layer

- **stdio** (default): Direct process communication for Claude Desktop
- **HTTP**: RESTful API with SSE for remote deployment

## Authentication System

OAuth 2.0 with refresh tokens, multi-account support, secure storage in `~/.config/google-calendar-mcp/tokens.json`.

## Handler Architecture

- `src/handlers/core/` - Individual tool handlers extending `BaseToolHandler`
- `src/tools/registry.ts` - Auto-registration system discovers and registers handlers
- `src/schemas/` - Input validation and type definitions

## Request Flow

```
Client → Transport → Schema Validation → Handler → Google API → Response
```

## MCP Tools

The server provides calendar management tools that LLMs can use for calendar operations:

### Available Tools

- `list-calendars` - List all available calendars
- `list-events` - List events with date filtering  
- `search-events` - Search events by text query
- `create-event` - Create new calendar events
- `update-event` - Update existing events
- `delete-event` - Delete events
- `get-freebusy` - Check availability across calendars
- `list-colors` - List available event colors
- `get-current-time` - Get current system time and timezone information

## Key Features

- **Auto-registration**: Handlers automatically discovered
- **Multi-account**: Normal/test account support  
- **Rate limiting**: Respects Google Calendar quotas
- **Batch operations**: Efficient multi-calendar queries
- **Recurring events**: Advanced modification scopes
- **Contextual resources**: Real-time date/time information
```

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

```json
{
  "name": "@cocal/google-calendar-mcp",
  "version": "2.0.6",
  "description": "Google Calendar MCP Server with extensive support for calendar management",
  "type": "module",
  "main": "build/index.js",
  "bin": {
    "google-calendar-mcp": "build/index.js"
  },
  "files": [
    "build/",
    "README.md",
    "LICENSE"
  ],
  "keywords": [
    "mcp",
    "model-context-protocol",
    "claude",
    "google-calendar",
    "calendar",
    "ai",
    "llm",
    "integration"
  ],
  "repository": {
    "type": "git",
    "url": "git+https://github.com/nspady/google-calendar-mcp.git"
  },
  "bugs": {
    "url": "https://github.com/nspady/google-calendar-mcp/issues"
  },
  "homepage": "https://github.com/nspady/google-calendar-mcp#readme",
  "author": "nspady",
  "license": "MIT",
  "scripts": {
    "start": "node build/index.js",
    "build": "node scripts/build.js",
    "auth": "node build/auth-server.js",
    "dev": "node scripts/dev.js",
    "lint": "tsc --noEmit -p tsconfig.lint.json",
    "test": "vitest run src/tests/unit",
    "test:watch": "vitest src/tests/unit",
    "test:integration": "vitest run src/tests/integration",
    "test:all": "vitest run src/tests",
    "test:coverage": "vitest run src/tests/unit --coverage",
    "start:http": "node build/index.js --transport http --port 3000",
    "start:http:public": "node build/index.js --transport http --port 3000 --host 0.0.0.0"
  },
  "dependencies": {
    "@google-cloud/local-auth": "^3.0.1",
    "@modelcontextprotocol/sdk": "^1.12.1",
    "google-auth-library": "^9.15.0",
    "googleapis": "^144.0.0",
    "open": "^7.4.2",
    "zod": "^3.22.4",
    "zod-to-json-schema": "^3.24.5"
  },
  "devDependencies": {
    "@anthropic-ai/sdk": "^0.52.0",
    "@types/node": "^20.10.4",
    "@vitest/coverage-v8": "^3.1.1",
    "esbuild": "^0.25.0",
    "openai": "^4.104.0",
    "typescript": "^5.3.3",
    "vitest": "^3.1.1"
  }
}

```

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

```typescript
export interface TransportConfig {
  type: 'stdio' | 'http';
  port?: number;
  host?: string;
}

export interface ServerConfig {
  transport: TransportConfig;
  debug?: boolean;
}

export function parseArgs(args: string[]): ServerConfig {
  // Start with environment variables as base config
  const config: ServerConfig = {
    transport: {
      type: (process.env.TRANSPORT as 'stdio' | 'http') || 'stdio',
      port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
      host: process.env.HOST || '127.0.0.1'
    },
    debug: process.env.DEBUG === 'true' || false
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    
    switch (arg) {
      case '--transport':
        const transport = args[++i];
        if (transport === 'stdio' || transport === 'http') {
          config.transport.type = transport;
        }
        break;
      case '--port':
        config.transport.port = parseInt(args[++i], 10);
        break;
      case '--host':
        config.transport.host = args[++i];
        break;
      case '--debug':
        config.debug = true;
        break;
      case '--help':
        process.stderr.write(`
Google Calendar MCP Server

Usage: node build/index.js [options]

Options:
  --transport <type>        Transport type: stdio (default) | http
  --port <number>          Port for HTTP transport (default: 3000)
  --host <string>          Host for HTTP transport (default: 127.0.0.1)
  --debug                  Enable debug logging
  --help                   Show this help message

Environment Variables:
  TRANSPORT               Transport type: stdio | http
  PORT                   Port for HTTP transport
  HOST                   Host for HTTP transport
  DEBUG                  Enable debug logging (true/false)

Examples:
  node build/index.js                              # stdio (local use)
  node build/index.js --transport http --port 3000 # HTTP server
  PORT=3000 TRANSPORT=http node build/index.js     # Using env vars
        `);
        process.exit(0);
    }
  }

  return config;
} 
```

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

```typescript
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { OAuth2Client } from "google-auth-library";
import { BaseToolHandler } from "./BaseToolHandler.js";
import { calendar_v3 } from 'googleapis';
import { buildSingleEventFieldMask } from "../../utils/field-mask-builder.js";
import { createStructuredResponse } from "../../utils/response-builder.js";
import { GetEventResponse, convertGoogleEventToStructured } from "../../types/structured-responses.js";

interface GetEventArgs {
    calendarId: string;
    eventId: string;
    fields?: string[];
}

export class GetEventHandler extends BaseToolHandler {
    async runTool(args: GetEventArgs, oauth2Client: OAuth2Client): Promise<CallToolResult> {
        const validArgs = args;
        
        try {
            const event = await this.getEvent(oauth2Client, validArgs);
            
            if (!event) {
                throw new Error(`Event with ID '${validArgs.eventId}' not found in calendar '${validArgs.calendarId}'.`);
            }
            
            const response: GetEventResponse = {
                event: convertGoogleEventToStructured(event, validArgs.calendarId)
            };
            
            return createStructuredResponse(response);
        } catch (error) {
            throw this.handleGoogleApiError(error);
        }
    }

    private async getEvent(
        client: OAuth2Client,
        args: GetEventArgs
    ): Promise<calendar_v3.Schema$Event | null> {
        const calendar = this.getCalendar(client);
        
        const fieldMask = buildSingleEventFieldMask(args.fields);
        
        try {
            const response = await calendar.events.get({
                calendarId: args.calendarId,
                eventId: args.eventId,
                ...(fieldMask && { fields: fieldMask })
            });
            
            return response.data;
        } catch (error: any) {
            // Handle 404 as a not found case
            if (error?.code === 404 || error?.response?.status === 404) {
                return null;
            }
            throw error;
        }
    }
}
```

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

```yaml
name: CI

on:
  push:
    branches: [ main, 'feature/**' ]
  pull_request:
    branches: [ main ]

jobs:
  # Job 1: Test and Coverage (includes build, quality checks, and coverage reporting)
  test-and-coverage:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: ['18']
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Setup Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
        
    - name: Install dependencies
      run: npm ci
      
    - name: Run unit tests with coverage
      run: |
        echo "🧪 Running unit tests with coverage..."
        echo "======================================"
        npm run test:coverage | tee test-output.log
        
        echo ""
        echo "📊 Test Results Summary:"
        echo "======================="
        
        # Extract test results from the log
        TEST_FILES=$(grep "Test Files" test-output.log | tail -1 | sed 's/.*Test Files[[:space:]]*//' | sed 's/[[:space:]]*passed.*//')
        TESTS_PASSED=$(grep "Tests" test-output.log | tail -1 | sed 's/.*Tests[[:space:]]*//' | sed 's/[[:space:]]*passed.*//')
        
        echo "✅ Test Files: $TEST_FILES passed"
        echo "✅ Total Tests: $TESTS_PASSED passed"
        echo "📁 Unit Test Files: $(find src/tests/unit -name '*.test.ts' | wc -l | tr -d ' ') files"
        echo "📁 Source Files: $(find src -name '*.ts' -not -path 'src/tests/*' | wc -l | tr -d ' ') files"
        
        echo ""
        echo "📈 Coverage Summary (from detailed report above):"
        echo "================================================="
        echo "• Lines, Functions, Branches, and Statements coverage shown in table above"
        echo "• Full HTML report available in coverage/index.html artifact"
        
        # Clean up temp file
        rm -f test-output.log
      env:
        NODE_ENV: test
        
    - name: Upload coverage reports
      uses: actions/upload-artifact@v4
      with:
        name: coverage-report
        path: coverage/
        retention-days: 30


```

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

```typescript
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { OAuth2Client } from "google-auth-library";
import { BaseToolHandler } from "./BaseToolHandler.js";
import { calendar_v3 } from "googleapis";
import { createStructuredResponse } from "../../utils/response-builder.js";
import { ListColorsResponse } from "../../types/structured-responses.js";

export class ListColorsHandler extends BaseToolHandler {
    async runTool(_: any, oauth2Client: OAuth2Client): Promise<CallToolResult> {
        const colors = await this.listColors(oauth2Client);
        
        const response: ListColorsResponse = {
            event: {},
            calendar: {}
        };
        
        // Convert event colors
        if (colors.event) {
            for (const [id, color] of Object.entries(colors.event)) {
                response.event[id] = {
                    background: color.background || '',
                    foreground: color.foreground || ''
                };
            }
        }
        
        // Convert calendar colors
        if (colors.calendar) {
            for (const [id, color] of Object.entries(colors.calendar)) {
                response.calendar[id] = {
                    background: color.background || '',
                    foreground: color.foreground || ''
                };
            }
        }
        
        return createStructuredResponse(response);
    }

    private async listColors(client: OAuth2Client): Promise<calendar_v3.Schema$Colors> {
        try {
            const calendar = this.getCalendar(client);
            const response = await calendar.colors.get();
            if (!response.data) throw new Error('Failed to retrieve colors');
            return response.data;
        } catch (error) {
            throw this.handleGoogleApiError(error);
        }
    }

    /**
     * Formats the color information into a user-friendly string.
     */
    private formatColorList(colors: calendar_v3.Schema$Colors): string {
        const eventColors = colors.event || {};
        return Object.entries(eventColors)
            .map(([id, colorInfo]) => `Color ID: ${id} - ${colorInfo.background} (background) / ${colorInfo.foreground} (foreground)`)
            .join("\n");
    }
}

```

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

```typescript
import { OAuth2Client } from 'google-auth-library';
import * as fs from 'fs/promises';
import { getKeysFilePath, generateCredentialsErrorMessage, OAuthCredentials } from './utils.js';

async function loadCredentialsFromFile(): Promise<OAuthCredentials> {
  const keysContent = await fs.readFile(getKeysFilePath(), "utf-8");
  const keys = JSON.parse(keysContent);

  if (keys.installed) {
    // Standard OAuth credentials file format
    const { client_id, client_secret, redirect_uris } = keys.installed;
    return { client_id, client_secret, redirect_uris };
  } else if (keys.client_id && keys.client_secret) {
    // Direct format
    return {
      client_id: keys.client_id,
      client_secret: keys.client_secret,
      redirect_uris: keys.redirect_uris || ['http://localhost:3000/oauth2callback']
    };
  } else {
    throw new Error('Invalid credentials file format. Expected either "installed" object or direct client_id/client_secret fields.');
  }
}

async function loadCredentialsWithFallback(): Promise<OAuthCredentials> {
  // Load credentials from file (CLI param, env var, or default path)
  try {
    return await loadCredentialsFromFile();
  } catch (fileError) {
    // Generate helpful error message
    const errorMessage = generateCredentialsErrorMessage();
    throw new Error(`${errorMessage}\n\nOriginal error: ${fileError instanceof Error ? fileError.message : fileError}`);
  }
}

export async function initializeOAuth2Client(): Promise<OAuth2Client> {
  // Always use real OAuth credentials - no mocking.
  // Unit tests should mock at the handler level, integration tests need real credentials.
  try {
    const credentials = await loadCredentialsWithFallback();
    
    // Use the first redirect URI as the default for the base client
    return new OAuth2Client({
      clientId: credentials.client_id,
      clientSecret: credentials.client_secret,
      redirectUri: credentials.redirect_uris[0],
    });
  } catch (error) {
    throw new Error(`Error loading OAuth keys: ${error instanceof Error ? error.message : error}`);
  }
}

export async function loadCredentials(): Promise<{ client_id: string; client_secret: string }> {
  try {
    const credentials = await loadCredentialsWithFallback();
    
    if (!credentials.client_id || !credentials.client_secret) {
        throw new Error('Client ID or Client Secret missing in credentials.');
    }
    return {
      client_id: credentials.client_id,
      client_secret: credentials.client_secret
    };
  } catch (error) {
    throw new Error(`Error loading credentials: ${error instanceof Error ? error.message : error}`);
  }
}
```

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

```typescript
import { describe, it, expect } from 'vitest';
import { hasTimezoneInDatetime, convertToRFC3339, createTimeObject } from '../../../handlers/utils/datetime.js';

describe('Datetime Utilities', () => {
  describe('hasTimezoneInDatetime', () => {
    it('should return true for timezone-aware datetime strings', () => {
      expect(hasTimezoneInDatetime('2024-01-01T10:00:00Z')).toBe(true);
      expect(hasTimezoneInDatetime('2024-01-01T10:00:00+05:00')).toBe(true);
      expect(hasTimezoneInDatetime('2024-01-01T10:00:00-08:00')).toBe(true);
    });

    it('should return false for timezone-naive datetime strings', () => {
      expect(hasTimezoneInDatetime('2024-01-01T10:00:00')).toBe(false);
      expect(hasTimezoneInDatetime('2024-01-01 10:00:00')).toBe(false);
    });
  });

  describe('convertToRFC3339', () => {
    it('should return timezone-aware datetime unchanged', () => {
      const datetime = '2024-01-01T10:00:00Z';
      expect(convertToRFC3339(datetime, 'America/Los_Angeles')).toBe(datetime);
    });

    it('should return timezone-aware datetime with offset unchanged', () => {
      const datetime = '2024-01-01T10:00:00-08:00';
      expect(convertToRFC3339(datetime, 'America/Los_Angeles')).toBe(datetime);
    });

    it('should convert timezone-naive datetime using fallback timezone', () => {
      const datetime = '2024-06-15T14:30:00';
      const result = convertToRFC3339(datetime, 'UTC');
      
      // Should result in a timezone-aware string (the exact time depends on system timezone)
      expect(result).toMatch(/2024-06-15T\d{2}:\d{2}:\d{2}Z/);
      expect(result).not.toBe(datetime); // Should be different from input
    });

    it('should fallback to UTC for invalid timezone conversion', () => {
      const datetime = '2024-01-01T10:00:00';
      const result = convertToRFC3339(datetime, 'Invalid/Timezone');
      
      // Should fallback to UTC
      expect(result).toBe('2024-01-01T10:00:00Z');
    });
  });

  describe('createTimeObject', () => {
    it('should create time object without timeZone for timezone-aware datetime', () => {
      const datetime = '2024-01-01T10:00:00Z';
      const result = createTimeObject(datetime, 'America/Los_Angeles');
      
      expect(result).toEqual({
        dateTime: datetime
      });
    });

    it('should create time object with timeZone for timezone-naive datetime', () => {
      const datetime = '2024-01-01T10:00:00';
      const timezone = 'America/Los_Angeles';
      const result = createTimeObject(datetime, timezone);
      
      expect(result).toEqual({
        dateTime: datetime,
        timeZone: timezone
      });
    });
  });
});
```

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

```typescript
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { OAuth2Client } from "google-auth-library";
import { BaseToolHandler } from "./BaseToolHandler.js";
import { calendar_v3 } from "googleapis";
import { ListCalendarsResponse } from "../../types/structured-responses.js";
import { createStructuredResponse } from "../../utils/response-builder.js";

export class ListCalendarsHandler extends BaseToolHandler {
    async runTool(_: any, oauth2Client: OAuth2Client): Promise<CallToolResult> {
        const calendars = await this.listCalendars(oauth2Client);

        const response: ListCalendarsResponse = {
            calendars: calendars.map(cal => ({
                id: cal.id || '',
                summary: cal.summary ?? undefined,
                description: cal.description ?? undefined,
                location: cal.location ?? undefined,
                timeZone: cal.timeZone ?? undefined,
                summaryOverride: cal.summaryOverride ?? undefined,
                colorId: cal.colorId ?? undefined,
                backgroundColor: cal.backgroundColor ?? undefined,
                foregroundColor: cal.foregroundColor ?? undefined,
                hidden: cal.hidden ?? undefined,
                selected: cal.selected ?? undefined,
                accessRole: cal.accessRole ?? undefined,
                defaultReminders: cal.defaultReminders?.map(r => ({
                    method: (r.method as 'email' | 'popup') || 'popup',
                    minutes: r.minutes || 0
                })),
                notificationSettings: cal.notificationSettings ? {
                    notifications: cal.notificationSettings.notifications?.map(n => ({
                        type: n.type ?? undefined,
                        method: n.method ?? undefined
                    }))
                } : undefined,
                primary: cal.primary ?? undefined,
                deleted: cal.deleted ?? undefined,
                conferenceProperties: cal.conferenceProperties ? {
                    allowedConferenceSolutionTypes: cal.conferenceProperties.allowedConferenceSolutionTypes ?? undefined
                } : undefined
            })),
            totalCount: calendars.length
        };

        return createStructuredResponse(response);
    }

    private async listCalendars(client: OAuth2Client): Promise<calendar_v3.Schema$CalendarListEntry[]> {
        try {
            const calendar = this.getCalendar(client);
            const response = await calendar.calendarList.list();
            return response.data.items || [];
        } catch (error) {
            throw this.handleGoogleApiError(error);
        }
    }
}

```

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

```typescript
import { initializeOAuth2Client } from './auth/client.js';
import { AuthServer } from './auth/server.js';

async function runAuthServer() {
  let authServer: AuthServer | null = null; // Keep reference for cleanup
  try {
    const oauth2Client = await initializeOAuth2Client();
    
    authServer = new AuthServer(oauth2Client);
    
    const success = await authServer.start(true);
    
    if (!success && !authServer.authCompletedSuccessfully) {
      process.stderr.write('Authentication failed. Could not start server or validate existing tokens.\n');
      process.exit(1);
    } else if (authServer.authCompletedSuccessfully) {
      process.stderr.write('Authentication successful.\n');
      process.exit(0);
    }
    
    // If we reach here, the server started and is waiting for the browser callback
    process.stderr.write('Authentication server started. Please complete the authentication in your browser...\n');
    

    process.stderr.write(`Waiting for OAuth callback on port ${authServer.getRunningPort()}...\n`);
    
    // Poll for completion or handle SIGINT
    let lastDebugLog = 0;
    const pollInterval = setInterval(async () => {
      try {
        if (authServer?.authCompletedSuccessfully) {
          process.stderr.write('Authentication completed successfully detected. Stopping server...\n');
          clearInterval(pollInterval);
          await authServer.stop();
          process.stderr.write('Authentication successful. Server stopped.\n');
          process.exit(0);
        } else {
          // Add debug logging every 10 seconds to show we're still waiting
          const now = Date.now();
          if (now - lastDebugLog > 10000) {
            process.stderr.write('Still waiting for authentication to complete...\n');
            lastDebugLog = now;
          }
        }
      } catch (error: unknown) {
        process.stderr.write(`Error in polling interval: ${error instanceof Error ? error.message : 'Unknown error'}\n`);
        clearInterval(pollInterval);
        if (authServer) await authServer.stop();
        process.exit(1);
      }
    }, 5000); // Check every second

    // Handle process termination (SIGINT)
    process.on('SIGINT', async () => {
      clearInterval(pollInterval); // Stop polling
      if (authServer) {
        await authServer.stop();
      }
      process.exit(0);
    });
    
  } catch (error: unknown) {
    process.stderr.write(`Authentication error: ${error instanceof Error ? error.message : 'Unknown error'}\n`);
    if (authServer) await authServer.stop(); // Attempt cleanup
    process.exit(1);
  }
}

// Run the auth server if this file is executed directly
if (import.meta.url.endsWith('auth-server.js')) {
  runAuthServer().catch((error: unknown) => {
    process.stderr.write(`Unhandled error: ${error instanceof Error ? error.message : 'Unknown error'}\n`);
    process.exit(1);
  });
}
```

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

```typescript
import { describe, it, expect, vi } from 'vitest';
import { CreateEventHandler } from '../../../handlers/core/CreateEventHandler.js';
import { OAuth2Client } from 'google-auth-library';
import { calendar_v3 } from 'googleapis';
import { CONFLICT_DETECTION_CONFIG } from '../../../services/conflict-detection/config.js';

describe('CreateEventHandler Blocking Logic', () => {
  const mockOAuth2Client = {
    getAccessToken: vi.fn().mockResolvedValue({ token: 'mock-token' })
  } as unknown as OAuth2Client;

  it('should show full event details when blocking due to high similarity', async () => {
    const handler = new CreateEventHandler();
    
    // Mock the conflict detection service
    const existingEvent: calendar_v3.Schema$Event = {
      id: 'existing-lunch-123',
      summary: 'Lunch with Josh',
      description: 'Monthly catch-up lunch',
      location: 'The Coffee Shop',
      start: { 
        dateTime: '2024-01-15T12:00:00-08:00',
        timeZone: 'America/Los_Angeles'
      },
      end: { 
        dateTime: '2024-01-15T13:00:00-08:00',
        timeZone: 'America/Los_Angeles'
      },
      attendees: [
        { email: '[email protected]', displayName: 'Josh', responseStatus: 'accepted' }
      ],
      htmlLink: 'https://calendar.google.com/calendar/event?eid=existing-lunch-123'
    };

    // Mock the checkConflicts method to return a high similarity duplicate
    vi.spyOn(handler['conflictDetectionService'], 'checkConflicts').mockResolvedValue({
      hasConflicts: true,
      duplicates: [{
        event: {
          id: 'existing-lunch-123',
          title: 'Lunch with Josh',
          url: 'https://calendar.google.com/calendar/event?eid=existing-lunch-123',
          similarity: 1.0 // 100% similar
        },
        fullEvent: existingEvent,
        calendarId: 'primary',
        suggestion: 'This appears to be a duplicate. Consider updating the existing event instead.'
      }],
      conflicts: []
    });

    // Mock getCalendarTimezone
    vi.spyOn(handler as any, 'getCalendarTimezone').mockResolvedValue('America/Los_Angeles');

    const args = {
      calendarId: 'primary',
      summary: 'Lunch with Josh',
      start: '2024-01-15T12:00:00',
      end: '2024-01-15T13:00:00',
      location: 'The Coffee Shop'
    };

    // Now it should throw an error instead of returning a text message
    await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow(
      'Duplicate event detected (100% similar). Event "Lunch with Josh" already exists. To create anyway, set allowDuplicates to true.'
    );
  });

  it('should use centralized threshold configuration', () => {
    // Verify that the config has the expected thresholds
    expect(CONFLICT_DETECTION_CONFIG.DEFAULT_DUPLICATE_THRESHOLD).toBe(0.7);
    expect(CONFLICT_DETECTION_CONFIG.DUPLICATE_THRESHOLDS.WARNING).toBe(0.7);
    expect(CONFLICT_DETECTION_CONFIG.DUPLICATE_THRESHOLDS.BLOCKING).toBe(0.95);
  });
});
```

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

```typescript
import { describe, it, expect } from 'vitest';
import { generateEventUrl, getEventUrl } from '../../../handlers/utils.js';
import { calendar_v3 } from 'googleapis';

describe('Event URL Utilities', () => {
    describe('generateEventUrl', () => {
        it('should generate a proper Google Calendar event URL', () => {
            const calendarId = '[email protected]';
            const eventId = 'abc123def456';
            const url = generateEventUrl(calendarId, eventId);
            
            expect(url).toBe('https://calendar.google.com/calendar/event?eid=abc123def456&cid=user%40example.com');
        });

        it('should properly encode special characters in calendar ID', () => {
            const calendarId = '[email protected]';
            const eventId = 'event123';
            const url = generateEventUrl(calendarId, eventId);
            
            expect(url).toBe('https://calendar.google.com/calendar/event?eid=event123&cid=user%40test-calendar.com');
        });

        it('should properly encode special characters in event ID', () => {
            const calendarId = '[email protected]';
            const eventId = 'event+with+special&chars';
            const url = generateEventUrl(calendarId, eventId);
            
            expect(url).toBe('https://calendar.google.com/calendar/event?eid=event%2Bwith%2Bspecial%26chars&cid=user%40example.com');
        });
    });

    describe('getEventUrl', () => {
        const mockEvent: calendar_v3.Schema$Event = {
            id: 'test123',
            summary: 'Test Event',
            start: { dateTime: '2024-03-15T10:00:00-07:00' },
            end: { dateTime: '2024-03-15T11:00:00-07:00' },
            location: 'Conference Room A',
            description: 'Test meeting'
        };

        it('should use htmlLink when available', () => {
            const eventWithHtmlLink = {
                ...mockEvent,
                htmlLink: 'https://calendar.google.com/event?eid=existing123'
            };
            
            const result = getEventUrl(eventWithHtmlLink);
            expect(result).toBe('https://calendar.google.com/event?eid=existing123');
        });

        it('should generate URL when htmlLink is not available but calendarId is provided', () => {
            const result = getEventUrl(mockEvent, '[email protected]');
            expect(result).toBe('https://calendar.google.com/calendar/event?eid=test123&cid=user%40example.com');
        });

        it('should return null when htmlLink is not available and calendarId is not provided', () => {
            const result = getEventUrl(mockEvent);
            expect(result).toBeNull();
        });

        it('should return null when event has no ID', () => {
            const eventWithoutId = { ...mockEvent, id: undefined };
            const result = getEventUrl(eventWithoutId, '[email protected]');
            expect(result).toBeNull();
        });
    });
});
```

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

```typescript
import { describe, it, expect } from 'vitest';
import { ToolRegistry } from '../../../tools/registry.js';

describe('Schema $ref Prevention Tests', () => {
  it('should not generate $ref references in JSON schemas, causes issues with Claude Desktop', () => {
    const tools = ToolRegistry.getToolsWithSchemas();
    
    // Convert each tool schema to JSON Schema and check for $ref
    for (const tool of tools) {
      const jsonSchema = JSON.stringify(tool.inputSchema);
      
      // Check for any $ref references
      const hasRef = jsonSchema.includes('"$ref"');
      
      if (hasRef) {
        console.error(`Tool "${tool.name}" contains $ref in schema:`, jsonSchema);
      }
      
      expect(hasRef).toBe(false);
    }
  });

  it('should have unique schema instances for similar parameters', () => {
    const tools = ToolRegistry.getToolsWithSchemas();
    
    // Find tools with timeMin/timeMax or start/end parameters
    const timeParams = [];
    
    for (const tool of tools) {
      const schemaStr = JSON.stringify(tool.inputSchema);
      if (schemaStr.includes('timeMin') || schemaStr.includes('timeMax') || 
          schemaStr.includes('"start"') || schemaStr.includes('"end"')) {
        timeParams.push(tool.name);
      }
    }
    
    // Ensure we're testing the right tools
    expect(timeParams.length).toBeGreaterThan(0);
    console.log('Tools with time parameters:', timeParams);
  });

  it('should detect if shared schema instances are reused', () => {
    // This test checks the source code structure to prevent regression
    const registryCode = require('fs').readFileSync(
      require('path').join(__dirname, '../../../tools/registry.ts'), 
      'utf8'
    );
    
    // Check for problematic patterns that could cause $ref generation
    // Note: Removed negative lookahead (?!\.) to catch ALL schema reuse including .optional() and .describe()
    const sharedSchemaUsage = [
      /timeMin:\s*[A-Z][a-zA-Z]*Schema/,     // timeMin: SomeSchema (any usage)
      /timeMax:\s*[A-Z][a-zA-Z]*Schema/,     // timeMax: SomeSchema
      /start:\s*[A-Z][a-zA-Z]*Schema/,       // start: SomeSchema
      /end:\s*[A-Z][a-zA-Z]*Schema/,         // end: SomeSchema
      /calendarId:\s*[A-Z][a-zA-Z]*Schema/,  // calendarId: SomeSchema
      /eventId:\s*[A-Z][a-zA-Z]*Schema/,     // eventId: SomeSchema
      /query:\s*[A-Z][a-zA-Z]*Schema/,       // query: SomeSchema
      /summary:\s*[A-Z][a-zA-Z]*Schema/,     // summary: SomeSchema
      /description:\s*[A-Z][a-zA-Z]*Schema/, // description: SomeSchema
      /location:\s*[A-Z][a-zA-Z]*Schema/,    // location: SomeSchema
      /colorId:\s*[A-Z][a-zA-Z]*Schema/,     // colorId: SomeSchema
      /reminders:\s*[A-Z][a-zA-Z]*Schema/,   // reminders: SomeSchema
      /attendees:\s*[A-Z][a-zA-Z]*Schema/,   // attendees: SomeSchema
      /email:\s*[A-Z][a-zA-Z]*Schema/        // email: SomeSchema
    ];
    
    for (const pattern of sharedSchemaUsage) {
      const matches = registryCode.match(pattern);
      if (matches) {
        console.error(`Found potentially problematic schema usage: ${matches[0]}`);
        expect(matches).toBeNull();
      }
    }
  });
});
```

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

```typescript
/**
 * Event ID validation utility for Google Calendar API
 */

/**
 * Validates a custom event ID according to Google Calendar requirements
 * @param eventId The event ID to validate
 * @returns true if valid, false otherwise
 */
export function isValidEventId(eventId: string): boolean {
  // Check length constraints (5-1024 characters)
  if (eventId.length < 5 || eventId.length > 1024) {
    return false;
  }
  
  // Check character constraints (base32hex encoding)
  // Google Calendar allows only: lowercase letters a-v and digits 0-9
  // Based on RFC2938 section 3.1.2
  const validPattern = /^[a-v0-9]+$/;
  return validPattern.test(eventId);
}

/**
 * Validates and throws an error if the event ID is invalid
 * @param eventId The event ID to validate
 * @throws Error if the event ID is invalid
 */
export function validateEventId(eventId: string): void {
  if (!isValidEventId(eventId)) {
    const errors: string[] = [];
    
    if (eventId.length < 5) {
      errors.push("must be at least 5 characters long");
    }
    
    if (eventId.length > 1024) {
      errors.push("must not exceed 1024 characters");
    }
    
    if (!/^[a-v0-9]+$/.test(eventId)) {
      errors.push("can only contain lowercase letters a-v and digits 0-9 (base32hex encoding)");
    }
    
    throw new Error(`Invalid event ID: ${errors.join(", ")}`);
  }
}

/**
 * Sanitizes a string to make it a valid event ID
 * Converts to base32hex encoding (lowercase a-v and 0-9 only)
 * @param input The input string to sanitize
 * @returns A valid event ID
 */
export function sanitizeEventId(input: string): string {
  // Convert to lowercase first
  let sanitized = input.toLowerCase();
  
  // Replace invalid characters:
  // - Keep digits 0-9 as is
  // - Map letters w-z to a-d (shift back)
  // - Map other characters to valid base32hex characters
  sanitized = sanitized.replace(/[^a-v0-9]/g, (char) => {
    // Map w-z to a-d
    if (char >= 'w' && char <= 'z') {
      return String.fromCharCode(char.charCodeAt(0) - 22); // w->a, x->b, y->c, z->d
    }
    // Map any other character to a default valid character
    return '';
  });
  
  // Remove any empty spaces from the mapping
  sanitized = sanitized.replace(/\s+/g, '');
  
  // Ensure minimum length
  if (sanitized.length < 5) {
    // Generate a base32hex timestamp
    const timestamp = Date.now().toString(32).replace(/[w-z]/g, (c) => 
      String.fromCharCode(c.charCodeAt(0) - 22)
    );
    
    if (sanitized.length === 0) {
      sanitized = `event${timestamp}`.substring(0, 26); // Match Google's 26-char format
    } else {
      sanitized = `${sanitized}${timestamp}`.substring(0, 26);
    }
  }
  
  // Ensure maximum length
  if (sanitized.length > 1024) {
    sanitized = sanitized.slice(0, 1024);
  }
  
  // Final validation - ensure only valid characters
  sanitized = sanitized.replace(/[^a-v0-9]/g, '');
  
  // If still too short after all operations, generate a default
  if (sanitized.length < 5) {
    // Generate a valid base32hex ID
    const now = Date.now();
    const base32hex = now.toString(32).replace(/[w-z]/g, (c) => 
      String.fromCharCode(c.charCodeAt(0) - 22)
    );
    sanitized = `ev${base32hex}`.substring(0, 26);
  }
  
  return sanitized;
}
```

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

```typescript
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { OAuth2Client } from "google-auth-library";
import { SearchEventsInput } from "../../tools/registry.js";
import { BaseToolHandler } from "./BaseToolHandler.js";
import { calendar_v3 } from 'googleapis';
import { convertToRFC3339 } from "../utils/datetime.js";
import { buildListFieldMask } from "../../utils/field-mask-builder.js";
import { createStructuredResponse, convertEventsToStructured } from "../../utils/response-builder.js";
import { SearchEventsResponse } from "../../types/structured-responses.js";

export class SearchEventsHandler extends BaseToolHandler {
    async runTool(args: any, oauth2Client: OAuth2Client): Promise<CallToolResult> {
        const validArgs = args as SearchEventsInput;
        const events = await this.searchEvents(oauth2Client, validArgs);
        
        const response: SearchEventsResponse = {
            events: convertEventsToStructured(events, validArgs.calendarId),
            totalCount: events.length,
            query: validArgs.query,
            calendarId: validArgs.calendarId
        };
        
        if (validArgs.timeMin || validArgs.timeMax) {
            const timezone = validArgs.timeZone || await this.getCalendarTimezone(oauth2Client, validArgs.calendarId);
            response.timeRange = {
                start: validArgs.timeMin ? convertToRFC3339(validArgs.timeMin, timezone) : '',
                end: validArgs.timeMax ? convertToRFC3339(validArgs.timeMax, timezone) : ''
            };
        }
        
        return createStructuredResponse(response);
    }

    private async searchEvents(
        client: OAuth2Client,
        args: SearchEventsInput
    ): Promise<calendar_v3.Schema$Event[]> {
        try {
            const calendar = this.getCalendar(client);
            
            // Determine timezone with correct precedence:
            // 1. Explicit timeZone parameter (highest priority)
            // 2. Calendar's default timezone (fallback)
            const timezone = args.timeZone || await this.getCalendarTimezone(client, args.calendarId);
            
            // Convert time boundaries to RFC3339 format for Google Calendar API
            // Note: convertToRFC3339 will still respect timezone in datetime string as highest priority
            const timeMin = convertToRFC3339(args.timeMin, timezone);
            const timeMax = convertToRFC3339(args.timeMax, timezone);
            
            const fieldMask = buildListFieldMask(args.fields);
            
            const response = await calendar.events.list({
                calendarId: args.calendarId,
                q: args.query,
                timeMin,
                timeMax,
                singleEvents: true,
                orderBy: 'startTime',
                ...(fieldMask && { fields: fieldMask }),
                ...(args.privateExtendedProperty && { privateExtendedProperty: args.privateExtendedProperty as any }),
                ...(args.sharedExtendedProperty && { sharedExtendedProperty: args.sharedExtendedProperty as any })
            });
            return response.data.items || [];
        } catch (error) {
            throw this.handleGoogleApiError(error);
        }
    }

}

```

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

```markdown
# Changelog

## [2.0.6](https://github.com/nspady/google-calendar-mcp/compare/v2.0.5...v2.0.6) (2025-10-22)


### Bug Fixes

* 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))

## [2.0.5](https://github.com/nspady/google-calendar-mcp/compare/v2.0.4...v2.0.5) (2025-10-19)


### Bug Fixes

* **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))

## [2.0.4](https://github.com/nspady/google-calendar-mcp/compare/v2.0.3...v2.0.4) (2025-10-15)


### Bug Fixes

* 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)

## [2.0.3](https://github.com/nspady/google-calendar-mcp/compare/v2.0.2...v2.0.3) (2025-10-15)


### Bug Fixes

* 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)

## [2.0.2](https://github.com/nspady/google-calendar-mcp/compare/v2.0.1...v2.0.2) (2025-10-14)


### Bug Fixes

* **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)

## [2.0.1](https://github.com/nspady/google-calendar-mcp/compare/v2.0.0...v2.0.1) (2025-10-13)


### Bug Fixes

* 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))
* 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))
* Support single-quoted JSON arrays in list-events calendarId ([d2af7cf](https://github.com/nspady/google-calendar-mcp/commit/d2af7cf99e3d090bceb388cbf10f7f9649100e3c))
* update publish workflow to use release-please ([47addc9](https://github.com/nspady/google-calendar-mcp/commit/47addc95cc04e552017afd7523638795bf9f9090))

## [2.0.2](https://github.com/nspady/google-calendar-mcp/compare/v2.0.1...v2.0.2) (2025-10-13)

### Bug Fixes

* 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))
* update publish workflow to use release-please ([47addc9](https://github.com/nspady/google-calendar-mcp/commit/47addc95cc04e552017afd7523638795bf9f9090))

## [2.0.1](https://github.com/nspady/google-calendar-mcp/compare/v2.0.0...v2.0.1) (2025-10-11)

### Bug Fixes

* 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))
* Support single-quoted JSON arrays in list-events calendarId ([d2af7cf](https://github.com/nspady/google-calendar-mcp/commit/d2af7cf99e3d090bceb388cbf10f7f9649100e3c))

```

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

```typescript
/**
 * Field mask builder for Google Calendar API partial response
 */

// Allowed fields that can be requested from Google Calendar API
export const ALLOWED_EVENT_FIELDS = [
  'id',
  'summary',
  'description',
  'start',
  'end',
  'location',
  'attendees',
  'colorId',
  'transparency',
  'extendedProperties',
  'reminders',
  'conferenceData',
  'attachments',
  'status',
  'htmlLink',
  'created',
  'updated',
  'creator',
  'organizer',
  'recurrence',
  'recurringEventId',
  'originalStartTime',
  'visibility',
  'iCalUID',
  'sequence',
  'hangoutLink',
  'anyoneCanAddSelf',
  'guestsCanInviteOthers',
  'guestsCanModify',
  'guestsCanSeeOtherGuests',
  'privateCopy',
  'locked',
  'source',
  'eventType'
] as const;

export type AllowedEventField = typeof ALLOWED_EVENT_FIELDS[number];

// Default fields always included
export const DEFAULT_EVENT_FIELDS: AllowedEventField[] = [
  'id',
  'summary',
  'start',
  'end',
  'status',
  'htmlLink',
  'location',
  'attendees'
];

/**
 * Validates that requested fields are allowed
 */
export function validateFields(fields: string[]): AllowedEventField[] {
  const validFields: AllowedEventField[] = [];
  const invalidFields: string[] = [];
  
  for (const field of fields) {
    if (ALLOWED_EVENT_FIELDS.includes(field as AllowedEventField)) {
      validFields.push(field as AllowedEventField);
    } else {
      invalidFields.push(field);
    }
  }
  
  if (invalidFields.length > 0) {
    throw new Error(`Invalid fields requested: ${invalidFields.join(', ')}. Allowed fields: ${ALLOWED_EVENT_FIELDS.join(', ')}`);
  }
  
  return validFields;
}

/**
 * Builds a Google Calendar API field mask for partial response
 * @param requestedFields Optional array of additional fields to include
 * @param includeDefaults Whether to include default fields (default: true)
 * @returns Field mask string for Google Calendar API
 */
export function buildEventFieldMask(
  requestedFields?: string[], 
  includeDefaults: boolean = true
): string | undefined {
  // If no custom fields requested and we should include defaults, return undefined
  // to let Google API return its default field set
  if (!requestedFields || requestedFields.length === 0) {
    return undefined;
  }
  
  // Validate requested fields
  const validFields = validateFields(requestedFields);
  
  // Combine with defaults if needed
  const allFields = includeDefaults 
    ? [...new Set([...DEFAULT_EVENT_FIELDS, ...validFields])]
    : validFields;
  
  // Build the field mask for events.list
  // Format: items(field1,field2,field3)
  return `items(${allFields.join(',')})`;
}

/**
 * Builds a field mask for a single event (events.get)
 */
export function buildSingleEventFieldMask(
  requestedFields?: string[],
  includeDefaults: boolean = true
): string | undefined {
  // If no custom fields requested, return undefined for default response
  if (!requestedFields || requestedFields.length === 0) {
    return undefined;
  }
  
  // Validate requested fields
  const validFields = validateFields(requestedFields);
  
  // Combine with defaults if needed
  const allFields = includeDefaults 
    ? [...new Set([...DEFAULT_EVENT_FIELDS, ...validFields])]
    : validFields;
  
  // For single event, just return comma-separated fields
  return allFields.join(',');
}

/**
 * Builds the full field mask parameter for list operations
 * Includes nextPageToken, nextSyncToken, etc.
 */
export function buildListFieldMask(
  requestedFields?: string[],
  includeDefaults: boolean = true
): string | undefined {
  // If no custom fields requested, return undefined for default response
  if (!requestedFields || requestedFields.length === 0) {
    return undefined;
  }
  
  const eventFieldMask = buildEventFieldMask(requestedFields, includeDefaults);
  if (!eventFieldMask) {
    return undefined;
  }
  
  // Include pagination tokens and other list metadata
  return `${eventFieldMask},nextPageToken,nextSyncToken,kind,etag,summary,updated,timeZone,accessRole,defaultReminders`;
}
```

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

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import http from "http";

export interface HttpTransportConfig {
  port?: number;
  host?: string;
}

export class HttpTransportHandler {
  private server: McpServer;
  private config: HttpTransportConfig;

  constructor(server: McpServer, config: HttpTransportConfig = {}) {
    this.server = server;
    this.config = config;
  }

  async connect(): Promise<void> {
    const port = this.config.port || 3000;
    const host = this.config.host || '127.0.0.1';

    // Configure transport for stateless mode to allow multiple initialization cycles
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined // Stateless mode - allows multiple initializations
    });

    await this.server.connect(transport);

    // Create HTTP server to handle the StreamableHTTP transport
    const httpServer = http.createServer(async (req, res) => {
      // Validate Origin header to prevent DNS rebinding attacks (MCP spec requirement)
      const origin = req.headers.origin;
      const allowedOrigins = [
        'http://localhost',
        'http://127.0.0.1',
        'https://localhost',
        'https://127.0.0.1'
      ];

      // For requests with Origin header, validate it
      if (origin && !allowedOrigins.some(allowed => origin.startsWith(allowed))) {
        res.writeHead(403, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
          error: 'Forbidden: Invalid origin',
          message: 'Origin header validation failed'
        }));
        return;
      }

      // Basic request size limiting (prevent DoS)
      const contentLength = parseInt(req.headers['content-length'] || '0', 10);
      const maxRequestSize = 10 * 1024 * 1024; // 10MB limit
      if (contentLength > maxRequestSize) {
        res.writeHead(413, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
          error: 'Payload Too Large',
          message: 'Request size exceeds maximum allowed size'
        }));
        return;
      }

      // Handle CORS
      res.setHeader('Access-Control-Allow-Origin', '*');
      res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
      res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id');
      
      if (req.method === 'OPTIONS') {
        res.writeHead(200);
        res.end();
        return;
      }

      // Validate Accept header for MCP requests (spec requirement)
      if (req.method === 'POST' || req.method === 'GET') {
        const acceptHeader = req.headers.accept;
        if (acceptHeader && !acceptHeader.includes('application/json') && !acceptHeader.includes('text/event-stream') && !acceptHeader.includes('*/*')) {
          res.writeHead(406, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify({
            error: 'Not Acceptable',
            message: 'Accept header must include application/json or text/event-stream'
          }));
          return;
        }
      }

      // Handle health check endpoint
      if (req.method === 'GET' && req.url === '/health') {
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
          status: 'healthy',
          server: 'google-calendar-mcp',
          timestamp: new Date().toISOString()
        }));
        return;
      }

      try {
        await transport.handleRequest(req, res);
      } catch (error) {
        process.stderr.write(`Error handling request: ${error instanceof Error ? error.message : error}\n`);
        if (!res.headersSent) {
          res.writeHead(500, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify({
            jsonrpc: '2.0',
            error: {
              code: -32603,
              message: 'Internal server error',
            },
            id: null,
          }));
        }
      }
    });

    httpServer.listen(port, host, () => {
      process.stderr.write(`Google Calendar MCP Server listening on http://${host}:${port}\n`);
    });
  }
} 
```

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

```typescript
import { calendar_v3 } from "googleapis";
import { EventTimeRange } from "./types.js";
import { EventSimilarityChecker } from "./EventSimilarityChecker.js";

export class ConflictAnalyzer {
  private similarityChecker: EventSimilarityChecker;
  
  constructor() {
    this.similarityChecker = new EventSimilarityChecker();
  }
  /**
   * Analyze overlap between two events
   * Uses consolidated overlap logic from EventSimilarityChecker
   */
  analyzeOverlap(event1: calendar_v3.Schema$Event, event2: calendar_v3.Schema$Event): {
    hasOverlap: boolean;
    duration?: string;
    percentage?: number;
    startTime?: string;
    endTime?: string;
  } {
    // Use consolidated overlap check
    const hasOverlap = this.similarityChecker.eventsOverlap(event1, event2);
    
    if (!hasOverlap) {
      return { hasOverlap: false };
    }
    
    // Get time ranges for detailed analysis
    const time1 = this.getEventTimeRange(event1);
    const time2 = this.getEventTimeRange(event2);
    
    if (!time1 || !time2) {
      return { hasOverlap: false };
    }
    
    // Calculate overlap details
    const overlapDuration = this.similarityChecker.calculateOverlapDuration(event1, event2);
    const overlapStart = new Date(Math.max(time1.start.getTime(), time2.start.getTime()));
    const overlapEnd = new Date(Math.min(time1.end.getTime(), time2.end.getTime()));
    
    // Calculate percentage of overlap relative to the first event
    const event1Duration = time1.end.getTime() - time1.start.getTime();
    const overlapPercentage = Math.round((overlapDuration / event1Duration) * 100);
    
    return {
      hasOverlap: true,
      duration: this.formatDuration(overlapDuration),
      percentage: overlapPercentage,
      startTime: overlapStart.toISOString(),
      endTime: overlapEnd.toISOString()
    };
  }

  /**
   * Get event time range
   */
  private getEventTimeRange(event: calendar_v3.Schema$Event): EventTimeRange | null {
    const startTime = event.start?.dateTime || event.start?.date;
    const endTime = event.end?.dateTime || event.end?.date;
    
    if (!startTime || !endTime) return null;
    
    const start = new Date(startTime);
    const end = new Date(endTime);
    
    // Check if it's an all-day event
    const isAllDay = !event.start?.dateTime && !!event.start?.date;
    
    return { start, end, isAllDay };
  }

  /**
   * Format duration in human-readable format
   */
  private formatDuration(milliseconds: number): string {
    const minutes = Math.floor(milliseconds / (1000 * 60));
    const hours = Math.floor(minutes / 60);
    const days = Math.floor(hours / 24);
    
    if (days > 0) {
      const remainingHours = hours % 24;
      return remainingHours > 0 
        ? `${days} day${days > 1 ? 's' : ''} ${remainingHours} hour${remainingHours > 1 ? 's' : ''}`
        : `${days} day${days > 1 ? 's' : ''}`;
    }
    
    if (hours > 0) {
      const remainingMinutes = minutes % 60;
      return remainingMinutes > 0
        ? `${hours} hour${hours > 1 ? 's' : ''} ${remainingMinutes} minute${remainingMinutes > 1 ? 's' : ''}`
        : `${hours} hour${hours > 1 ? 's' : ''}`;
    }
    
    return `${minutes} minute${minutes > 1 ? 's' : ''}`;
  }

  /**
   * Check if an event conflicts with a busy time slot
   */
  checkBusyConflict(event: calendar_v3.Schema$Event, busySlot: { start?: string | null; end?: string | null }): boolean {
    // Handle null values from Google's API
    const start = busySlot.start ?? undefined;
    const end = busySlot.end ?? undefined;
    
    if (!start || !end) return false;
    
    // Convert busy slot to event format for consistency
    const busyEvent: calendar_v3.Schema$Event = {
      start: { dateTime: start },
      end: { dateTime: end }
    };
    
    return this.similarityChecker.eventsOverlap(event, busyEvent);
  }

  /**
   * Filter events that overlap with a given time range
   */
  findOverlappingEvents(
    events: calendar_v3.Schema$Event[],
    targetEvent: calendar_v3.Schema$Event
  ): calendar_v3.Schema$Event[] {
    return events.filter(event => {
      // Skip the same event
      if (event.id === targetEvent.id) return false;
      
      // Skip cancelled events
      if (event.status === 'cancelled') return false;
      
      // Use consolidated overlap check
      return this.similarityChecker.eventsOverlap(targetEvent, event);
    });
  }
}
```

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

```typescript
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import { fileURLToPath } from 'url';
import { getSecureTokenPath as getSharedSecureTokenPath, getLegacyTokenPath as getSharedLegacyTokenPath, getAccountMode as getSharedAccountMode } from './paths.js';

// Helper to get the project root directory reliably
function getProjectRoot(): string {
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
  // In build output (e.g., build/bundle.js), __dirname is .../build
  // Go up ONE level to get the project root
  const projectRoot = path.join(__dirname, ".."); // Corrected: Go up ONE level
  return path.resolve(projectRoot); // Ensure absolute path
}

// Get the current account mode (normal or test) - delegates to shared implementation
export function getAccountMode(): 'normal' | 'test' {
  return getSharedAccountMode() as 'normal' | 'test';
}

// Helper to detect if we're running in a test environment
function isRunningInTestEnvironment(): boolean {
  // Simple and reliable: just check NODE_ENV
  return process.env.NODE_ENV === 'test';
}

// Returns the absolute path for the saved token file - delegates to shared implementation
export function getSecureTokenPath(): string {
  return getSharedSecureTokenPath();
}

// Returns the legacy token path for backward compatibility - delegates to shared implementation  
export function getLegacyTokenPath(): string {
  return getSharedLegacyTokenPath();
}

// Returns the absolute path for the GCP OAuth keys file with priority:
// 1. Environment variable GOOGLE_OAUTH_CREDENTIALS (highest priority)
// 2. Default file path (lowest priority)
export function getKeysFilePath(): string {
  // Priority 1: Environment variable
  const envCredentialsPath = process.env.GOOGLE_OAUTH_CREDENTIALS;
  if (envCredentialsPath) {
    return path.resolve(envCredentialsPath);
  }
  
  // Priority 2: Default file path
  const projectRoot = getProjectRoot();
  const keysPath = path.join(projectRoot, "gcp-oauth.keys.json");
  return keysPath; // Already absolute from getProjectRoot
}

// Helper to determine if we're currently in test mode
export function isTestMode(): boolean {
  return getAccountMode() === 'test';
}

// Interface for OAuth credentials
export interface OAuthCredentials {
  client_id: string;
  client_secret: string;
  redirect_uris: string[];
}

// Interface for credentials file with project_id
export interface OAuthCredentialsWithProject {
  installed?: {
    project_id?: string;
    client_id?: string;
    client_secret?: string;
    redirect_uris?: string[];
  };
  project_id?: string;
  client_id?: string;
  client_secret?: string;
  redirect_uris?: string[];
}

// Get project ID from OAuth credentials file
// Returns undefined if credentials file doesn't exist, is invalid, or missing project_id
export function getCredentialsProjectId(): string | undefined {
  try {
    // Use existing helper to get credentials file path
    const credentialsPath = getKeysFilePath();

    if (!fs.existsSync(credentialsPath)) {
      return undefined;
    }

    const credentialsContent = fs.readFileSync(credentialsPath, 'utf-8');
    const credentials: OAuthCredentialsWithProject = JSON.parse(credentialsContent);

    // Extract project_id from installed format or direct format
    if (credentials.installed?.project_id) {
      return credentials.installed.project_id;
    } else if (credentials.project_id) {
      return credentials.project_id;
    }

    return undefined;
  } catch (error) {
    // If we can't read project ID, return undefined (backward compatibility)
    return undefined;
  }
}

// Generate helpful error message for missing credentials
export function generateCredentialsErrorMessage(): string {
  return `
OAuth credentials not found. Please provide credentials using one of these methods:

1. Environment variable:
   Set GOOGLE_OAUTH_CREDENTIALS to the path of your credentials file:
   export GOOGLE_OAUTH_CREDENTIALS="/path/to/gcp-oauth.keys.json"

2. Default file path:
   Place your gcp-oauth.keys.json file in the package root directory.

Token storage:
- Tokens are saved to: ${getSecureTokenPath()}
- To use a custom token location, set GOOGLE_CALENDAR_MCP_TOKEN_PATH environment variable

To get OAuth credentials:
1. Go to the Google Cloud Console (https://console.cloud.google.com/)
2. Create or select a project
3. Enable the Google Calendar API
4. Create OAuth 2.0 credentials
5. Download the credentials file as gcp-oauth.keys.json
`.trim();
}

```

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

```typescript
import { describe, it, expect } from 'vitest';
import {
  buildEventFieldMask,
  buildSingleEventFieldMask,
  buildListFieldMask,
  validateFields,
  ALLOWED_EVENT_FIELDS,
  DEFAULT_EVENT_FIELDS
} from '../../../utils/field-mask-builder.js';

describe('Field Mask Builder', () => {
  describe('validateFields', () => {
    it('should accept valid fields', () => {
      const validFields = ['description', 'colorId', 'transparency'];
      const result = validateFields(validFields);
      expect(result).toEqual(validFields);
    });

    it('should reject invalid fields', () => {
      const invalidFields = ['invalid', 'notafield'];
      expect(() => validateFields(invalidFields)).toThrow('Invalid fields requested: invalid, notafield');
    });

    it('should handle mixed valid and invalid fields', () => {
      const mixedFields = ['description', 'invalid', 'colorId'];
      expect(() => validateFields(mixedFields)).toThrow('Invalid fields requested: invalid');
    });

    it('should accept all allowed fields', () => {
      const result = validateFields([...ALLOWED_EVENT_FIELDS]);
      expect(result).toEqual(ALLOWED_EVENT_FIELDS);
    });
  });

  describe('buildEventFieldMask', () => {
    it('should return undefined when no fields requested', () => {
      expect(buildEventFieldMask()).toBeUndefined();
      expect(buildEventFieldMask([])).toBeUndefined();
    });

    it('should build field mask with requested fields and defaults', () => {
      const fields = ['description', 'colorId'];
      const result = buildEventFieldMask(fields);
      expect(result).toContain('items(');
      expect(result).toContain('description');
      expect(result).toContain('colorId');
      // Should also include defaults
      DEFAULT_EVENT_FIELDS.forEach(field => {
        expect(result).toContain(field);
      });
    });

    it('should build field mask without defaults when specified', () => {
      const fields = ['description', 'colorId'];
      const result = buildEventFieldMask(fields, false);
      expect(result).toBe('items(description,colorId)');
    });

    it('should handle duplicate fields', () => {
      const fields = ['description', 'description', 'id', 'summary'];
      const result = buildEventFieldMask(fields);
      // Should deduplicate
      const fieldCount = (result?.match(/description/g) || []).length;
      expect(fieldCount).toBe(1);
    });

    it('should throw for invalid fields', () => {
      const fields = ['description', 'invalidfield'];
      expect(() => buildEventFieldMask(fields)).toThrow('Invalid fields requested: invalidfield');
    });
  });

  describe('buildSingleEventFieldMask', () => {
    it('should return undefined when no fields requested', () => {
      expect(buildSingleEventFieldMask()).toBeUndefined();
      expect(buildSingleEventFieldMask([])).toBeUndefined();
    });

    it('should build comma-separated field list with defaults', () => {
      const fields = ['description', 'colorId'];
      const result = buildSingleEventFieldMask(fields);
      expect(result).not.toContain('items(');
      expect(result).toContain('description');
      expect(result).toContain('colorId');
      // Should also include defaults
      DEFAULT_EVENT_FIELDS.forEach(field => {
        expect(result).toContain(field);
      });
    });

    it('should build field list without defaults when specified', () => {
      const fields = ['description', 'colorId'];
      const result = buildSingleEventFieldMask(fields, false);
      expect(result).toBe('description,colorId');
    });
  });

  describe('buildListFieldMask', () => {
    it('should return undefined when no fields requested', () => {
      expect(buildListFieldMask()).toBeUndefined();
      expect(buildListFieldMask([])).toBeUndefined();
    });

    it('should include list metadata fields', () => {
      const fields = ['description'];
      const result = buildListFieldMask(fields);
      expect(result).toContain('nextPageToken');
      expect(result).toContain('nextSyncToken');
      expect(result).toContain('kind');
      expect(result).toContain('etag');
      expect(result).toContain('timeZone');
      expect(result).toContain('accessRole');
    });

    it('should include event fields in items()', () => {
      const fields = ['description', 'colorId'];
      const result = buildListFieldMask(fields);
      expect(result).toContain('items(');
      expect(result).toContain('description');
      expect(result).toContain('colorId');
    });
  });
});
```

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

```typescript
import { BaseToolHandler } from './BaseToolHandler.js';
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { OAuth2Client } from "google-auth-library";
import { GetFreeBusyInput } from "../../tools/registry.js";
import { FreeBusyResponse as GoogleFreeBusyResponse } from '../../schemas/types.js';
import { FreeBusyResponse } from '../../types/structured-responses.js';
import { createStructuredResponse } from '../../utils/response-builder.js';
import { McpError } from '@modelcontextprotocol/sdk/types.js';
import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { convertToRFC3339 } from '../utils/datetime.js';

export class FreeBusyEventHandler extends BaseToolHandler {
  async runTool(args: any, oauth2Client: OAuth2Client): Promise<CallToolResult> {
    const validArgs = args as GetFreeBusyInput;

    if(!this.isLessThanThreeMonths(validArgs.timeMin,validArgs.timeMax)){
      throw new McpError(
        ErrorCode.InvalidRequest,
        "The time gap between timeMin and timeMax must be less than 3 months"
      );
    }

    const result = await this.queryFreeBusy(oauth2Client, validArgs);

    const response: FreeBusyResponse = {
      timeMin: validArgs.timeMin,
      timeMax: validArgs.timeMax,
      calendars: this.formatCalendarsData(result)
    };

    return createStructuredResponse(response);
  }

  private async queryFreeBusy(
    client: OAuth2Client,
    args: GetFreeBusyInput
  ): Promise<GoogleFreeBusyResponse> {
    try {
      const calendar = this.getCalendar(client);

      // Determine timezone with correct precedence:
      // 1. Explicit timeZone parameter (highest priority)
      // 2. Primary calendar's default timezone (fallback)
      // 3. UTC if calendar timezone retrieval fails
      let timezone: string;
      if (args.timeZone) {
        timezone = args.timeZone;
      } else {
        try {
          timezone = await this.getCalendarTimezone(client, 'primary');
        } catch (error) {
          // If we can't get the primary calendar's timezone, fall back to UTC
          // This can happen if the user doesn't have access to 'primary' calendar
          timezone = 'UTC';
        }
      }

      // Convert time boundaries to RFC3339 format for Google Calendar API
      // This handles both timezone-aware and timezone-naive datetime strings
      const timeMin = convertToRFC3339(args.timeMin, timezone);
      const timeMax = convertToRFC3339(args.timeMax, timezone);

      // Build request body
      // Note: The timeZone parameter affects the response format, not request interpretation
      // Since timeMin/timeMax are in RFC3339 (with timezone), they're unambiguous
      // But we include timeZone so busy periods in the response use consistent timezone
      const requestBody: any = {
        timeMin,
        timeMax,
        items: args.calendars,
        timeZone: timezone, // Always include to ensure response consistency
      };

      // Only add optional expansion fields if provided
      if (args.groupExpansionMax !== undefined) {
        requestBody.groupExpansionMax = args.groupExpansionMax;
      }
      if (args.calendarExpansionMax !== undefined) {
        requestBody.calendarExpansionMax = args.calendarExpansionMax;
      }

      const response = await calendar.freebusy.query({
        requestBody,
      });
      return response.data as GoogleFreeBusyResponse;
    } catch (error) {
      throw this.handleGoogleApiError(error);
    }
  }

  private isLessThanThreeMonths(timeMin: string, timeMax: string): boolean {
    const minDate = new Date(timeMin);
    const maxDate = new Date(timeMax);

    const diffInMilliseconds = maxDate.getTime() - minDate.getTime();
    const threeMonthsInMilliseconds = 3 * 30 * 24 * 60 * 60 * 1000;

    return diffInMilliseconds <= threeMonthsInMilliseconds;
  }

  private formatCalendarsData(response: GoogleFreeBusyResponse): Record<string, {
    busy: Array<{ start: string; end: string }>;
    errors?: Array<{ domain?: string; reason?: string }>;
  }> {
    const calendars: Record<string, any> = {};

    if (response.calendars) {
      for (const [calId, calData] of Object.entries(response.calendars) as [string, any][]) {
        calendars[calId] = {
          busy: calData.busy?.map((slot: any) => ({
            start: slot.start,
            end: slot.end
          })) || []
        };

        if (calData.errors?.length > 0) {
          calendars[calId].errors = calData.errors.map((err: any) => ({
            domain: err.domain,
            reason: err.reason
          }));
        }
      }
    }

    return calendars;
  }
}

```

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

```markdown
# Advanced Usage Guide

This guide covers advanced features and use cases for the Google Calendar MCP Server.

## Multi-Account Support

The server supports managing multiple Google accounts (e.g., personal and a test calendar).

### Setup Multiple Accounts

```bash
# Authenticate normal account
npm run auth

# Authenticate test account
npm run auth:test

# Check status
npm run account:status
```

### Account Management Commands

```bash
npm run account:clear:normal    # Clear normal account tokens
npm run account:clear:test      # Clear test account tokens
npm run account:migrate         # Migrate from old token format
```

### Using Multiple Accounts

The server intelligently determines which account to use:
- Normal operations use your primary account
- Integration tests automatically use the test account
- Accounts are isolated and secure

## Batch Operations

### List Events from Multiple Calendars

Request events from several calendars simultaneously:

```
"Show me all events from my work, personal, and team calendars for next week"
```

The server will:
1. Query all specified calendars in parallel
2. Merge and sort results chronologically
3. Handle different timezones correctly

### Batch Event Creation

Create multiple related events:

```
"Schedule a 3-part training series every Monday at 2pm for the next 3 weeks"
```

## Recurring Events

### Modification Scopes

When updating recurring events, you can specify the scope:

1. **This event only**: Modify a single instance
   ```
   "Move tomorrow's standup to 11am (just this one)"
   ```

2. **This and following events**: Modify from a specific date forward
   ```
   "Change all future team meetings to 30 minutes starting next week"
   ```

3. **All events**: Modify the entire series
   ```
   "Update the location for all weekly reviews to Conference Room B"
   ```

### Complex Recurrence Patterns

The server supports all Google Calendar recurrence rules:
- Daily, weekly, monthly, yearly patterns
- Custom intervals (e.g., every 3 days)
- Specific days (e.g., every Tuesday and Thursday)
- End conditions (after N occurrences or by date)

## Timezone Handling

All times require explicit timezone information:
- Automatic timezone detection based on your calendar settings
- Support for scheduling across timezones
- Proper handling of daylight saving time transitions

### Availability Checking

Find optimal meeting times:

```
"Find a 90-minute slot next week when both my work and personal calendars are free, preferably in the afternoon"
```

## Working with Images

### Extract Events from Screenshots

```
"Add this event to my calendar [attach screenshot]"
```

Supported formats: PNG, JPEG, GIF

The server can extract:
- Date and time information
- Event titles and descriptions
- Location details
- Attendee lists

### Best Practices for Image Recognition

1. Ensure text is clear and readable
2. Include full date/time information in the image
3. Highlight or circle important details
4. Use high contrast images

## Advanced Search

### Search Operators

- **By attendee**: "meetings with [email protected]"
- **By location**: "events at headquarters"
- **By time range**: "morning meetings this month"
- **By status**: "tentative events this week"

### Complex Queries

Combine multiple criteria:

```
"Find all meetings with the sales team at the main office that are longer than an hour in the next two weeks"
```

## Calendar Analysis

### Meeting Patterns

```
"How much time did I spend in meetings last week?"
"What percentage of my meetings are recurring?"
"Which day typically has the most meetings?"
```
## Performance Optimization

### Rate Limiting

Built-in protection against API limits:
- Automatic retry with exponential backoff in batch operations
- HTTP transport includes basic rate limiting (100 requests per IP per 15 minutes)

## Integration Examples

### Daily Schedule

```
"Show me today's events and check for any scheduling conflicts between all my calendars"
```

### Weekly Planning

```
"Look at next week and suggest the best times for deep work blocks of at least 2 hours"
```

### Meeting Preparation

```
"For each meeting tomorrow, tell me who's attending, what the agenda is, and what materials I should review"
```

## Security Considerations

### Permission Scopes

The server only requests necessary permissions:
- `calendar.events`: Full event management
- Never requests email or profile access
- No access to other Google services

### Token Security

- Tokens encrypted at rest
- Automatic token refresh
- Secure credential storage
- No tokens in logs or debug output

## Debugging

### Enable Debug Logging

```bash
DEBUG=mcp:* npm start
```

### Common Issues

1. **Token refresh failures**: Check network connectivity
2. **API quota exceeded**: Implement backoff strategies
3. **Timezone mismatches**: Ensure consistent timezone usage

See [Troubleshooting Guide](troubleshooting.md) for detailed solutions.
```

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

```typescript
/**
 * Datetime utilities for Google Calendar MCP Server
 * Provides timezone handling and datetime conversion utilities
 */

/**
 * Checks if a datetime string includes timezone information
 * @param datetime ISO 8601 datetime string
 * @returns True if timezone is included, false if timezone-naive
 */
export function hasTimezoneInDatetime(datetime: string): boolean {
    return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(datetime);
}

/**
 * Converts a flexible datetime string to RFC3339 format required by Google Calendar API
 * 
 * Precedence rules:
 * 1. If datetime already has timezone info (Z or ±HH:MM), use as-is
 * 2. If datetime is timezone-naive, interpret it as local time in fallbackTimezone and convert to UTC
 * 
 * @param datetime ISO 8601 datetime string (with or without timezone)
 * @param fallbackTimezone Timezone to use if datetime is timezone-naive (IANA format)
 * @returns RFC3339 formatted datetime string in UTC
 */
export function convertToRFC3339(datetime: string, fallbackTimezone: string): string {
    if (hasTimezoneInDatetime(datetime)) {
        // Already has timezone, use as-is
        return datetime;
    } else {
        // Timezone-naive, interpret as local time in fallbackTimezone and convert to UTC
        try {
            // Parse the datetime components
            const match = datetime.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})$/);
            if (!match) {
                throw new Error('Invalid datetime format');
            }
            
            const [, year, month, day, hour, minute, second] = match.map(Number);
            
            // Create a temporary date in UTC to get the baseline
            const utcDate = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
            
            // Find what UTC time corresponds to the desired local time in the target timezone
            // We do this by binary search approach or by using the timezone offset
            const targetDate = convertLocalTimeToUTC(year, month - 1, day, hour, minute, second, fallbackTimezone);
            
            return targetDate.toISOString().replace(/\.000Z$/, 'Z');
        } catch (error) {
            // Fallback: if timezone conversion fails, append Z for UTC
            return datetime + 'Z';
        }
    }
}

/**
 * Convert a local time in a specific timezone to UTC
 */
function convertLocalTimeToUTC(year: number, month: number, day: number, hour: number, minute: number, second: number, timezone: string): Date {
    // Create a date that we'll use to find the correct UTC time
    // Start with the assumption that it's in UTC
    let testDate = new Date(Date.UTC(year, month, day, hour, minute, second));
    
    // Get what this UTC time looks like in the target timezone
    const options: Intl.DateTimeFormatOptions = {
        timeZone: timezone,
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit',
        hour12: false
    };
    
    // Format the test date in the target timezone
    const formatter = new Intl.DateTimeFormat('sv-SE', options);
    const formattedInTargetTZ = formatter.format(testDate);
    
    // Parse the formatted result to see what time it shows
    const [datePart, timePart] = formattedInTargetTZ.split(' ');
    const [targetYear, targetMonth, targetDay] = datePart.split('-').map(Number);
    const [targetHour, targetMinute, targetSecond] = timePart.split(':').map(Number);
    
    // Calculate the difference between what we want and what we got
    const wantedTime = new Date(year, month, day, hour, minute, second).getTime();
    const actualTime = new Date(targetYear, targetMonth - 1, targetDay, targetHour, targetMinute, targetSecond).getTime();
    const offsetMs = wantedTime - actualTime;
    
    // Adjust the UTC time by the offset
    return new Date(testDate.getTime() + offsetMs);
}

/**
 * Creates a time object for Google Calendar API, handling both timezone-aware and timezone-naive datetime strings
 * Also handles all-day events by using 'date' field instead of 'dateTime'
 * @param datetime ISO 8601 datetime string (with or without timezone)
 * @param fallbackTimezone Timezone to use if datetime is timezone-naive (IANA format)
 * @returns Google Calendar API time object
 */
export function createTimeObject(datetime: string, fallbackTimezone: string): { dateTime?: string; date?: string; timeZone?: string } {
    // Check if this is a date-only string (all-day event)
    // Date-only format: YYYY-MM-DD (no time component)
    if (!/T/.test(datetime)) {
        // This is a date-only string, use the 'date' field for all-day event
        return { date: datetime };
    }
    
    // This is a datetime string with time component
    if (hasTimezoneInDatetime(datetime)) {
        // Timezone included in datetime - use as-is, no separate timeZone property needed
        return { dateTime: datetime };
    } else {
        // Timezone-naive datetime - use fallback timezone
        return { dateTime: datetime, timeZone: fallbackTimezone };
    }
}
```

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

```markdown
# Authentication Setup Guide

This guide provides detailed instructions for setting up Google OAuth 2.0 authentication for the Google Calendar MCP Server.

## Google Cloud Setup

### 1. Create a Google Cloud Project

1. Go to the [Google Cloud Console](https://console.cloud.google.com)
2. Click "Select a project" → "New Project"
3. Enter a project name (e.g., "Calendar MCP")
4. Click "Create"

### 2. Enable the Google Calendar API

1. In your project, go to "APIs & Services" → "Library"
2. Search for "Google Calendar API"
3. Click on it and press "Enable"
4. Wait for the API to be enabled (usually takes a few seconds)

### 3. Create OAuth 2.0 Credentials

1. Go to "APIs & Services" → "Credentials"
2. Click "Create Credentials" → "OAuth client ID"
3. If prompted, configure the OAuth consent screen first:
   - Choose "External" user type
   - Fill in the required fields:
     - App name: "Calendar MCP" (or your choice)
     - User support email: Your email
     - Developer contact: Your email
   - Add scopes:
     - Click "Add or Remove Scopes"
     - Add: `https://www.googleapis.com/auth/calendar.events`
     - Or use the broader scope: `https://www.googleapis.com/auth/calendar`
   - Add test users:
     - Add your email address
     - **Important**: Wait 2-3 minutes for test users to propagate

4. Create the OAuth client:
   - Application type: **Desktop app** (Important!)
   - Name: "Calendar MCP Client"
   - Click "Create"

5. Download the credentials:
   - Click the download button (⬇️) next to your new client
   - Save as `gcp-oauth.keys.json`

## Credential File Format

Your credentials file should look like this:

```json
{
  "installed": {
    "client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",
    "client_secret": "YOUR_CLIENT_SECRET",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",
    "redirect_uris": ["http://localhost"]
  }
}
```

## Credential Storage Options

### Option 1: Environment Variable (Recommended)

Set the `GOOGLE_OAUTH_CREDENTIALS` environment variable to point to your credentials file:

```bash
export GOOGLE_OAUTH_CREDENTIALS="/path/to/your/gcp-oauth.keys.json"
```

In Claude Desktop config:
```json
{
  "mcpServers": {
    "google-calendar": {
      "command": "npx",
      "args": ["@cocal/google-calendar-mcp"],
      "env": {
        "GOOGLE_OAUTH_CREDENTIALS": "/path/to/your/gcp-oauth.keys.json"
      }
    }
  }
}
```

### Option 2: Default Location

Place the credentials file in the project root as `gcp-oauth.keys.json`.

## Token Storage

OAuth tokens are automatically stored in a secure location:

- **macOS/Linux**: `~/.config/google-calendar-mcp/tokens.json`
- **Windows**: `%APPDATA%\google-calendar-mcp\tokens.json`

To use a custom location, set:
```bash
export GOOGLE_CALENDAR_MCP_TOKEN_PATH="/custom/path/tokens.json"
```

## First-Time Authentication

1. Start Claude Desktop after configuration
2. The server will automatically open your browser for authentication
3. Sign in with your Google account
4. Grant the requested calendar permissions
5. You'll see a success message in the browser
6. Return to Claude - you're ready to use calendar features!

## Re-authentication

If your tokens expire or become invalid:

### For NPX Installation
If you're using the MCP via `npx` (e.g., in Claude Desktop):

```bash
# Set your credentials path first
export GOOGLE_OAUTH_CREDENTIALS="/path/to/your/gcp-oauth.keys.json"

# Run the auth command
npx @cocal/google-calendar-mcp auth
```

### For Local Installation
```bash
npm run auth
```

The server will guide you through the authentication flow again.

## Important Notes

### Test Mode Limitations

While your app is in test mode:
- OAuth tokens expire after 7 days
- Limited to test users you've explicitly added
- Perfect for personal use

### Avoiding Token Expiration

Test mode tokens expire after 7 days. For personal use, you can simply re-authenticate weekly using the commands above. 

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. 


### Security Best Practices

1. **Never commit credentials**: Add `gcp-oauth.keys.json` to `.gitignore`
2. **Secure file permissions**: 
   ```bash
   chmod 600 /path/to/gcp-oauth.keys.json
   ```
3. **Use environment variables**: Keeps credentials out of config files
4. **Regularly rotate**: Regenerate credentials if compromised

## Troubleshooting

### "Invalid credentials" error
- Ensure you selected "Desktop app" as the application type
- Check that the credentials file is valid JSON
- Verify the file path is correct

### "Access blocked" error
- Add your email as a test user in OAuth consent screen
- Wait 2-3 minutes for changes to propagate

### "Token expired" error
- Run `npm run auth` to re-authenticate
- Check if you're in test mode (7-day expiration)

See [Troubleshooting Guide](troubleshooting.md) for more issues and solutions.
```

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

```javascript
#!/usr/bin/env node

/**
 * Modern HTTP Client for Google Calendar MCP Server
 * 
 * This demonstrates how to connect to the Google Calendar MCP server
 * when it's running in StreamableHTTP transport mode. To test this
 * make sure you have the server running locally `npm run start:http`
 */

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

async function main() {
  const serverUrl = process.argv[2] || 'http://localhost:3000';
  
  console.log(`🔗 Connecting to Google Calendar MCP Server at: ${serverUrl}`);
  
  try {
    // First test health endpoint to ensure server is running
    console.log('🏥 Testing server health...');
    const healthResponse = await fetch(`${serverUrl}/health`, {
      headers: {
        'Accept': 'application/json'
      }
    });
    
    if (healthResponse.ok) {
      const healthData = await healthResponse.json();
      console.log('✅ Server is healthy:', healthData);
    } else {
      console.error('❌ Server health check failed');
      return;
    }

    // Create MCP client
    const client = new Client({
      name: "google-calendar-http-client",
      version: "1.0.0"
    }, {
      capabilities: {
        tools: {}
      }
    });

    // Skip direct initialization test to avoid double-initialization error
    console.log('\n🔍 Skipping direct initialization test...');

    // Connect with SDK transport
    console.log('\n🚀 Connecting with MCP SDK transport...');
    const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
    
    await client.connect(transport);
    console.log('✅ Connected to server');

    // List available tools
    console.log('\n📋 Listing available tools...');
    const tools = await client.listTools();
    console.log(`Found ${tools.tools.length} tools:`);
    
    tools.tools.forEach((tool, index) => {
      console.log(`  ${index + 1}. ${tool.name}`);
      console.log(`     Description: ${tool.description}`);
    });

    // Test some basic tools
    console.log('\n🛠️ Testing tools...');
    
    // Test list-calendars
    try {
      console.log('\n📅 Testing list-calendars...');
      const calendarsResult = await client.callTool({
        name: 'list-calendars',
        arguments: {}
      });
      console.log('✅ list-calendars successful');
      console.log('Result:', calendarsResult.content[0].text.substring(0, 300) + '...');
    } catch (error) {
      console.log('❌ list-calendars failed:', error.message);
    }

    // Test list-colors
    try {
      console.log('\n🎨 Testing list-colors...');
      const colorsResult = await client.callTool({
        name: 'list-colors',
        arguments: {}
      });
      console.log('✅ list-colors successful');
      console.log('Result:', colorsResult.content[0].text.substring(0, 300) + '...');
    } catch (error) {
      console.log('❌ list-colors failed:', error.message);
    }

    // Test list-events for primary calendar
    try {
      console.log('\n📆 Testing list-events...');
      
      // Create ISO strings using standard JavaScript toISOString()
      const now = new Date();
      const nextWeek = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
      
      // Use standard RFC 3339 format with milliseconds (now properly supported)
      const timeMin = now.toISOString().split('.')[0] + 'Z';
      const timeMax = nextWeek.toISOString().split('.')[0] + 'Z';
      
      console.log(`📅 Fetching events from ${timeMin} to ${timeMax}`);
      
      const eventsResult = await client.callTool({
        name: 'list-events',
        arguments: {
          calendarId: 'primary',
          timeMin: timeMin,
          timeMax: timeMax
        }
      });
      console.log('✅ list-events successful');
      console.log('Result:', eventsResult.content[0].text.substring(0, 500) + '...');
    } catch (error) {
      console.log('❌ list-events failed:', error.message);
    }

    // Close the connection
    console.log('\n🔒 Closing connection...');
    await client.close();
    console.log('✅ Connection closed');
    
    console.log('\n🎉 Google Calendar MCP client test completed!');

  } catch (error) {
    console.error('❌ Error:', error);
    
    if (error.message.includes('Authentication required')) {
      console.log('\n💡 Authentication required:');
      console.log('   Run: npm run auth');
      console.log('   Then restart the server: npm run start:http');
    } else if (error.message.includes('ECONNREFUSED')) {
      console.log('\n💡 Server not running:');
      console.log('   Start server: npm run start:http');
    } else {
      console.log('\n💡 Check that:');
      console.log('   1. Server is running (npm run start:http)');
      console.log('   2. Authentication is complete (npm run auth)');
      console.log('   3. Server URL is correct');
    }
    
    process.exit(1);
  }
}

// Handle graceful shutdown
process.on('SIGINT', () => {
  console.log('\n👋 Shutting down HTTP client...');
  process.exit(0);
});

main().catch(error => {
  console.error('❌ Unhandled error:', error);
  process.exit(1);
});

```

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

```typescript
import { fileURLToPath } from "url";
import { GoogleCalendarMcpServer } from './server.js';
import { parseArgs } from './config/TransportConfig.js';
import { readFileSync } from "fs";
import { join, dirname } from "path";

// Import modular components
import { initializeOAuth2Client } from './auth/client.js';
import { AuthServer } from './auth/server.js';

// Get package version
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJsonPath = join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const VERSION = packageJson.version;

// --- Main Application Logic --- 
async function main() {
  try {
    // Parse command line arguments
    const config = parseArgs(process.argv.slice(2));
    
    // Create and initialize the server
    const server = new GoogleCalendarMcpServer(config);
    await server.initialize();
    
    // Start the server with the appropriate transport
    await server.start();

  } catch (error: unknown) {
    process.stderr.write(`Failed to start server: ${error instanceof Error ? error.message : error}\n`);
    process.exit(1);
  }
}


// --- Command Line Interface ---
async function runAuthServer(): Promise<void> {
  // Use the same logic as auth-server.ts
  try {
    // Initialize OAuth client
    const oauth2Client = await initializeOAuth2Client();

    // Create and start the auth server
    const authServerInstance = new AuthServer(oauth2Client);

    // Start with browser opening (true by default)
    const success = await authServerInstance.start(true);

    if (!success && !authServerInstance.authCompletedSuccessfully) {
      // Failed to start and tokens weren't already valid
      process.stderr.write(
        "Authentication failed. Could not start server or validate existing tokens. Check port availability (3000-3004) and try again.\n"
      );
      process.exit(1);
    } else if (authServerInstance.authCompletedSuccessfully) {
      // Auth was successful (either existing tokens were valid or flow completed just now)
      process.stderr.write("Authentication successful.\n");
      process.exit(0); // Exit cleanly if auth is already done
    }

    // If we reach here, the server started and is waiting for the browser callback
    process.stderr.write(
      "Authentication server started. Please complete the authentication in your browser...\n"
    );

    // Wait for completion
    const intervalId = setInterval(async () => {
      if (authServerInstance.authCompletedSuccessfully) {
        clearInterval(intervalId);
        await authServerInstance.stop();
        process.stderr.write("Authentication completed successfully!\n");
        process.exit(0);
      }
    }, 1000);
  } catch (error) {
    process.stderr.write(`Authentication failed: ${error}\n`);
    process.exit(1);
  }
}

function showHelp(): void {
  process.stdout.write(`
Google Calendar MCP Server v${VERSION}

Usage:
  npx @cocal/google-calendar-mcp [command]

Commands:
  auth     Run the authentication flow
  start    Start the MCP server (default)
  version  Show version information
  help     Show this help message

Examples:
  npx @cocal/google-calendar-mcp auth
  npx @cocal/google-calendar-mcp start
  npx @cocal/google-calendar-mcp version
  npx @cocal/google-calendar-mcp

Environment Variables:
  GOOGLE_OAUTH_CREDENTIALS    Path to OAuth credentials file
`);
}

function showVersion(): void {
  process.stdout.write(`Google Calendar MCP Server v${VERSION}\n`);
}

// --- Exports & Execution Guard --- 
// Export main for testing or potential programmatic use
export { main, runAuthServer };

// Parse CLI arguments
function parseCliArgs(): { command: string | undefined } {
  const args = process.argv.slice(2);
  let command: string | undefined;

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    
    // Handle special version/help flags as commands
    if (arg === '--version' || arg === '-v' || arg === '--help' || arg === '-h') {
      command = arg;
      continue;
    }
    
    // Skip transport options and their values
    if (arg === '--transport' || arg === '--port' || arg === '--host') {
      i++; // Skip the next argument (the value)
      continue;
    }
    
    // Skip other flags
    if (arg === '--debug') {
      continue;
    }
    
    // Check for command (first non-option argument)
    if (!command && !arg.startsWith('--')) {
      command = arg;
      continue;
    }
  }

  return { command };
}

// CLI logic here (run always)
const { command } = parseCliArgs();

switch (command) {
  case "auth":
    runAuthServer().catch((error) => {
      process.stderr.write(`Authentication failed: ${error}\n`);
      process.exit(1);
    });
    break;
  case "start":
  case void 0:
    main().catch((error) => {
      process.stderr.write(`Failed to start server: ${error}\n`);
      process.exit(1);
    });
    break;
  case "version":
  case "--version":
  case "-v":
    showVersion();
    break;
  case "help":
  case "--help":
  case "-h":
    showHelp();
    break;
  default:
    process.stderr.write(`Unknown command: ${command}\n`);
    showHelp();
    process.exit(1);
}
```

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

```bash
#!/bin/bash

# Test script for Google Calendar MCP Server HTTP mode using curl
# This demonstrates basic HTTP requests to test the MCP server

SERVER_URL="${1:-http://localhost:3000}"
SESSION_ID="curl-test-session-$(date +%s)"

echo "🚀 Testing Google Calendar MCP Server at: $SERVER_URL"
echo "🆔 Using session ID: $SESSION_ID"
echo "=================================================="

# Test 1: Health check
echo -e "\n🏥 Testing health endpoint..."
curl -s "$SERVER_URL/health" | jq '.' || echo "Health check failed"

# Test 2: Initialize MCP session
echo -e "\n🤝 Testing MCP initialize..."

# MCP Initialize request
INIT_REQUEST='{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "tools": {}
    },
    "clientInfo": {
      "name": "curl-test-client",
      "version": "1.0.0"
    }
  }
}'

echo "Sending initialize request..."
INIT_RESPONSE=$(curl -s -X POST "$SERVER_URL" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: $SESSION_ID" \
  -d "$INIT_REQUEST")

# Try to parse as JSON, if that fails, check if it's SSE format
if echo "$INIT_RESPONSE" | jq '.' >/dev/null 2>&1; then
  # Direct JSON response - extract and parse the nested content
  echo "$INIT_RESPONSE" | jq -r '.result.content[0].text // empty' | jq '.' 2>/dev/null || echo "$INIT_RESPONSE" | jq '.'
elif echo "$INIT_RESPONSE" | grep -q "^data:"; then
  # SSE format - extract data and parse nested content
  echo "$INIT_RESPONSE" | grep "^data:" | sed 's/^data: //' | jq -r '.result.content[0].text // empty' | jq '.' 2>/dev/null
else
  echo "❌ Unknown response format"
  echo "$INIT_RESPONSE"
fi

# Check if initialization was successful
if echo "$INIT_RESPONSE" | grep -q "result\|initialize"; then
  echo "✅ Initialization successful"
else
  echo "❌ Initialization failed - stopping tests"
  exit 1
fi

# Test 3: List Tools request (after successful initialization)
echo -e "\n📋 Testing list tools..."
LIST_TOOLS_REQUEST='{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}'

TOOLS_RESPONSE=$(curl -s -X POST "$SERVER_URL" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: $SESSION_ID" \
  -d "$LIST_TOOLS_REQUEST")

# Parse response appropriately
if echo "$TOOLS_RESPONSE" | jq '.' >/dev/null 2>&1; then
  # Direct JSON - show the full tools list response (not nested content)
  echo "$TOOLS_RESPONSE" | jq '.result.tools[] | {name, description}'
elif echo "$TOOLS_RESPONSE" | grep -q "^data:"; then
  # SSE format
  echo "$TOOLS_RESPONSE" | grep "^data:" | sed 's/^data: //' | jq '.result.tools[] | {name, description}'
else
  echo "❌ List tools failed - unknown format"
  echo "$TOOLS_RESPONSE"
fi

# Test 4: Call list-calendars tool
echo -e "\n📅 Testing list-calendars tool..."

LIST_CALENDARS_REQUEST='{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "list-calendars",
    "arguments": {}
  }
}'

CALENDARS_RESPONSE=$(curl -s -X POST "$SERVER_URL" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: $SESSION_ID" \
  -d "$LIST_CALENDARS_REQUEST")

# Parse response appropriately
if echo "$CALENDARS_RESPONSE" | jq '.' >/dev/null 2>&1; then
  # Extract the nested JSON from content[0].text and parse it
  echo "$CALENDARS_RESPONSE" | jq -r '.result.content[0].text' | jq '.calendars[] | {id, summary, timeZone, accessRole}'
elif echo "$CALENDARS_RESPONSE" | grep -q "^data:"; then
  # SSE format - extract data, then nested content
  echo "$CALENDARS_RESPONSE" | grep "^data:" | sed 's/^data: //' | jq -r '.result.content[0].text' | jq '.calendars[] | {id, summary, timeZone, accessRole}'
else
  echo "❌ List calendars failed - unknown format"
  echo "$CALENDARS_RESPONSE"
fi

# Test 5: Call list-colors tool
echo -e "\n🎨 Testing list-colors tool..."

LIST_COLORS_REQUEST='{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "list-colors",
    "arguments": {}
  }
}'

COLORS_RESPONSE=$(curl -s -X POST "$SERVER_URL" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: $SESSION_ID" \
  -d "$LIST_COLORS_REQUEST")

# Parse response appropriately
if echo "$COLORS_RESPONSE" | jq '.' >/dev/null 2>&1; then
  # Extract nested JSON and display color summary
  echo "$COLORS_RESPONSE" | jq -r '.result.content[0].text' | jq '{
    eventColors: .event | length,
    calendarColors: .calendar | length,
    sampleEventColor: .event["1"],
    sampleCalendarColor: .calendar["1"]
  }'
elif echo "$COLORS_RESPONSE" | grep -q "^data:"; then
  # SSE format
  echo "$COLORS_RESPONSE" | grep "^data:" | sed 's/^data: //' | jq -r '.result.content[0].text' | jq '{
    eventColors: .event | length,
    calendarColors: .calendar | length,
    sampleEventColor: .event["1"],
    sampleCalendarColor: .calendar["1"]
  }'
else
  echo "❌ List colors failed - unknown format"
  echo "$COLORS_RESPONSE"
fi

echo -e "\n✅ HTTP testing completed!"
echo -e "\n💡 To test with different server URL: $0 http://your-server:port"

```

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

```markdown
# Deployment Guide

This guide covers deploying the Google Calendar MCP Server for remote access via HTTP transport.

## Transport Modes

### stdio Transport (Default)
- Local use only
- Direct communication with Claude Desktop
- No network exposure
- Automatic authentication handling

### HTTP Transport
- Remote deployment capable
- Server-Sent Events (SSE) for real-time communication
- Built-in security features
- Suitable for cloud deployment

## HTTP Server Features

- ✅ **CORS Support**: Basic cross-origin access support
- ✅ **Health Monitoring**: Basic health check endpoint
- ✅ **Graceful Shutdown**: Proper resource cleanup
- ✅ **Origin Validation**: DNS rebinding protection

## Local HTTP Deployment

### Basic HTTP Server

```bash
# Start on localhost only (default port 3000)
npm run start:http
```

### Public HTTP Server

```bash
# Listen on all interfaces (0.0.0.0)
npm run start:http:public
```

### Environment Variables

```bash
PORT=3000                    # Server port
HOST=localhost              # Bind address
TRANSPORT=http              # Transport mode
```

## Docker Deployment

### Using Docker Compose (Recommended)

```bash
# stdio mode  
docker compose up -d server

# HTTP mode
docker compose --profile http up -d
```

See [Docker Guide](docker.md) for complete setup instructions.

### Using Docker Run

```bash
# Create volume for token storage
docker volume create mcp-tokens

# stdio mode
docker run -i \
  -v ./gcp-oauth.keys.json:/usr/src/app/gcp-oauth.keys.json:ro \
  -v mcp-tokens:/home/nodejs/.config/google-calendar-mcp \
  -e TRANSPORT=stdio \
  --name calendar-mcp \
  google-calendar-mcp

# HTTP mode
docker run -d \
  -p 3000:3000 \
  -v ./gcp-oauth.keys.json:/usr/src/app/gcp-oauth.keys.json:ro \
  -v mcp-tokens:/home/nodejs/.config/google-calendar-mcp \
  -e TRANSPORT=http \
  -e HOST=0.0.0.0 \
  --name calendar-mcp \
  google-calendar-mcp

```

### Building Custom Image

Use the provided Dockerfile which includes proper user setup and token storage:

```bash
# Build image
docker build -t google-calendar-mcp .

# Run with authentication
docker run -it google-calendar-mcp npm run auth
```

## Cloud Deployment

### Google Cloud Run

```bash
# Build and push image
gcloud builds submit --tag gcr.io/PROJECT-ID/calendar-mcp

# Deploy
gcloud run deploy calendar-mcp \
  --image gcr.io/PROJECT-ID/calendar-mcp \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated \
  --set-env-vars="TRANSPORT=http"
```

### AWS ECS

1. Push image to ECR
2. Create task definition with environment variables
3. Deploy service with ALB

### Heroku

```bash
# Create app
heroku create your-calendar-mcp

# Set buildpack
heroku buildpacks:set heroku/nodejs

# Configure
heroku config:set TRANSPORT=http
heroku config:set GOOGLE_OAUTH_CREDENTIALS=./gcp-oauth.keys.json

# Deploy
git push heroku main
```

## Security Configuration

### HTTPS/TLS

Always use HTTPS in production:

1. **Behind a Reverse Proxy** (Recommended)
   ```nginx
   server {
       listen 443 ssl;
       server_name calendar-mcp.example.com;
       
       ssl_certificate /path/to/cert.pem;
       ssl_certificate_key /path/to/key.pem;
       
       location / {
           proxy_pass http://localhost:3000;
           proxy_http_version 1.1;
           proxy_set_header Upgrade $http_upgrade;
           proxy_set_header Connection '';
           proxy_set_header Host $host;
           proxy_set_header X-Real-IP $remote_addr;
       }
   }
   ```

2. **Direct TLS** (Not built-in)
   For TLS support, use a reverse proxy like nginx or a cloud load balancer with SSL termination.


### Authentication Flow

1. Client connects to HTTP endpoint
2. Server redirects to Google OAuth
3. User authenticates with Google
4. Server stores tokens securely
5. Client receives session token
6. All requests use session token

## Monitoring

### Health Checks

```bash
# Health check
curl http://localhost:3000/health
```

### Logging

```bash
# Enable debug logging
DEBUG=mcp:* npm run start:http

# JSON logging for production
NODE_ENV=production npm run start:http
```


## Production Checklist

**OAuth App Setup:**
- [ ] **Publish OAuth app to production in Google Cloud Console**
- [ ] **Set up proper redirect URIs for your domain**
- [ ] **Use production OAuth credentials (not test/development)**
- [ ] **Consider submitting for verification to remove user warnings**

**Infrastructure:**
- [ ] Use HTTPS/TLS encryption
- [ ] Configure reverse proxy for production
- [ ] Set up SSL termination
- [ ] Set up monitoring/alerting for authentication failures
- [ ] Configure log aggregation
- [ ] Implement backup strategy for token storage
- [ ] Test disaster recovery and re-authentication procedures
- [ ] Review security headers
- [ ] Enable graceful shutdown

**Note**: The 7-day token expiration is resolved by publishing your OAuth app to production in Google Cloud Console.

## Troubleshooting

### Connection Issues
- Check firewall rules
- Verify CORS configuration
- Test with curl first

### Authentication Failures
- Ensure credentials are accessible
- Check token permissions
- Verify redirect URIs

### Performance Problems
- Enable caching headers
- Use CDN for static assets
- Monitor memory usage

See [Troubleshooting Guide](troubleshooting.md) for more solutions.
```

--------------------------------------------------------------------------------
/docs/docker.md:
--------------------------------------------------------------------------------

```markdown
# Docker Deployment Guide

Simple, production-ready Docker setup for the Google Calendar MCP Server. Follow the quick start guide if you already have the project downloaded.

## Quick Start 

```bash
# 1. Place OAuth credentials in project root
# * optional if you have already placed the file in the root of this project folder
cp /path/to/your/gcp-oauth.keys.json ./gcp-oauth.keys.json

# Ensure the file has correct permissions for Docker to read
chmod 644 ./gcp-oauth.keys.json

# 2. Configure environment (optional - uses stdio mode by default)
# The .env.example file contains all defaults. Copy if customization needed:
cp .env.example .env

# 3. Build and start the server
docker compose up -d

# 4. Authenticate (one-time setup)
# This will show the authentication URL that needs to be
# visited to give authorization to the application.
# Visit the URL and complete the OAuth process.
# Note: This runs the built auth-server.js (build happens during docker build)
docker compose exec calendar-mcp npm run auth
# Note: This step only needs to be done once unless the app is in testing mode
# in which case the tokens expire after 7 days 

# 5. Add to Claude Desktop config (see stdio Mode section below)
```

## Two Modes

The server supports two transport modes: **stdio** (for local Claude Desktop) and **HTTP** (for remote/web access).

### stdio Mode (Recommended for Claude Desktop)
**Direct process integration for Claude Desktop:**

#### Step 1: Initial Setup
```bash
# Clone and setup
git clone https://github.com/nspady/google-calendar-mcp.git
cd google-calendar-mcp

# Place your OAuth credentials in the project root
cp /path/to/your/gcp-oauth.keys.json ./gcp-oauth.keys.json

# Ensure the file has correct permissions for Docker to read
chmod 644 ./gcp-oauth.keys.json

# Build and start the container
docker compose up -d

# Authenticate (one-time setup)
# Note: This runs the built auth-server.js (build happens during docker build)
docker compose exec calendar-mcp npm run auth
```

#### Step 2: Claude Desktop Configuration
Add to your Claude Desktop config file:

**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "google-calendar": {
      "command": "docker",
      "args": [
        "run", "--rm", "-i",
        "--mount", "type=bind,src=/absolute/path/to/your/gcp-oauth.keys.json,dst=/app/gcp-oauth.keys.json",
        "--mount", "type=volume,src=google-calendar-mcp_calendar-tokens,dst=/home/nodejs/.config/google-calendar-mcp",
        "calendar-mcp"
      ]
    }
  }
}
```

**⚠️ Important**: Replace `/absolute/path/to/your/gcp-oauth.keys.json` with the actual absolute path to your credentials file.

#### Step 3: Restart Claude Desktop
Restart Claude Desktop to load the new configuration. The server should now work without authentication prompts.

### HTTP Mode
**For testing, debugging, and web integration (Claude Desktop uses stdio):**

#### Step 1: Configure Environment
```bash
# Clone and setup
git clone https://github.com/nspady/google-calendar-mcp.git
cd google-calendar-mcp

# Place your OAuth credentials in the project root
cp /path/to/your/gcp-oauth.keys.json ./gcp-oauth.keys.json

# Ensure the file has correct permissions for Docker to read
chmod 644 ./gcp-oauth.keys.json

# Configure for HTTP mode
# Copy .env.example which has defaults (TRANSPORT=stdio, HOST=0.0.0.0, PORT=3000)
cp .env.example .env

# Change TRANSPORT to http (other defaults are already correct)
# Update TRANSPORT=stdio to TRANSPORT=http in .env

```

#### Step 2: Start and Authenticate
```bash
# Build and start the server in HTTP mode
docker compose up -d

# Authenticate (one-time setup)
# Note: This runs the built auth-server.js (build happens during docker build)
docker compose exec calendar-mcp npm run auth
# This will show authentication URLs (visit the displayed URL)
# This step only needs to be done once unless the app is in testing mode
# in which case the tokens expire after 7 days 

# Verify server is running
curl http://localhost:3000/health
# Should return: {"status":"healthy","server":"google-calendar-mcp","timestamp":"YYYY-MM-DDT00:00:00.000"}
```

#### Step 3: Test with cURL Example
```bash
# Run comprehensive HTTP tests
bash examples/http-with-curl.sh

# Or test specific endpoint
bash examples/http-with-curl.sh http://localhost:3000
```

#### Step 4: Claude Desktop HTTP Configuration
Add to your Claude Desktop config file:

**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "google-calendar": {
      "command": "mcp-client",
      "args": ["http://localhost:3000"]
    }
  }
}
```

**Note**: HTTP mode requires the container to be running (`docker compose up -d`)

## Development: Updating Code

If you modify the source code and want to see your changes reflected in the Docker container:

```bash
# Rebuild the Docker image and restart the container
docker compose build && docker compose up -d

# Verify changes are applied (check timestamp updates)
curl http://localhost:3000/health
```

**Why rebuild?** The Docker image contains a built snapshot of your code. Changes to source files won't appear until you rebuild the image with `docker compose build`.
```

--------------------------------------------------------------------------------
/src/tests/unit/handlers/duplicate-event-display.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from 'vitest';
import { formatConflictWarnings } from '../../../handlers/utils.js';
import { ConflictCheckResult } from '../../../services/conflict-detection/types.js';
import { calendar_v3 } from 'googleapis';

describe('Duplicate Event Display', () => {
  it('should show full formatted event details for duplicates with calendarId', () => {
    const duplicateEvent: calendar_v3.Schema$Event = {
      id: 'dup123',
      summary: 'Weekly Team Standup',
      description: 'Weekly sync with the engineering team',
      location: 'Conference Room B',
      start: { 
        dateTime: '2024-01-15T10:00:00-08:00',
        timeZone: 'America/Los_Angeles'
      },
      end: { 
        dateTime: '2024-01-15T10:30:00-08:00',
        timeZone: 'America/Los_Angeles'
      },
      attendees: [
        { email: '[email protected]', displayName: 'Alice', responseStatus: 'accepted' },
        { email: '[email protected]', displayName: 'Bob', responseStatus: 'needsAction' }
      ],
      organizer: {
        email: '[email protected]',
        displayName: 'Team Lead'
      },
      recurrence: ['RRULE:FREQ=WEEKLY;BYDAY=MO'],
      reminders: {
        useDefault: false,
        overrides: [{ method: 'popup', minutes: 10 }]
      }
    };

    const conflicts: ConflictCheckResult = {
      hasConflicts: true,
      duplicates: [{
        event: {
          id: 'dup123',
          title: 'Weekly Team Standup',
          url: 'https://calendar.google.com/calendar/event?eid=dup123&cid=primary',
          similarity: 0.85
        },
        fullEvent: duplicateEvent,
        calendarId: 'primary',
        suggestion: 'This event is very similar to an existing one. Is this intentional?'
      }],
      conflicts: []
    };

    const formatted = formatConflictWarnings(conflicts);
    
    // Verify the header
    expect(formatted).toContain('POTENTIAL DUPLICATES DETECTED');
    expect(formatted).toContain('85% similar');
    expect(formatted).toContain('This event is very similar to an existing one. Is this intentional?');
    
    // Verify full event details are shown
    expect(formatted).toContain('Existing event details:');
    expect(formatted).toContain('Event: Weekly Team Standup');
    expect(formatted).toContain('Event ID: dup123');
    expect(formatted).toContain('Description: Weekly sync with the engineering team');
    expect(formatted).toContain('Location: Conference Room B');
    
    // Verify time formatting
    expect(formatted).toContain('Start:');
    expect(formatted).toContain('End:');
    expect(formatted).toContain('PST'); // Should show timezone
    
    // Verify attendees
    expect(formatted).toContain('Guests: Alice (accepted), Bob (pending)');
    
    // Verify the URL is generated with calendarId
    expect(formatted).toContain('View: https://calendar.google.com/calendar/event?eid=dup123&cid=primary');
  });

  it('should show multiple duplicates with their full details', () => {
    const dup1: calendar_v3.Schema$Event = {
      id: 'morning-standup',
      summary: 'Team Standup',
      start: { dateTime: '2024-01-15T09:00:00Z' },
      end: { dateTime: '2024-01-15T09:15:00Z' }
    };

    const dup2: calendar_v3.Schema$Event = {
      id: 'daily-standup',
      summary: 'Daily Team Standup',
      description: 'Quick sync',
      start: { dateTime: '2024-01-15T09:00:00Z' },
      end: { dateTime: '2024-01-15T09:30:00Z' },
      location: 'Zoom'
    };

    const conflicts: ConflictCheckResult = {
      hasConflicts: true,
      duplicates: [
        {
          event: {
            id: 'morning-standup',
            title: 'Team Standup',
            similarity: 0.75
          },
          fullEvent: dup1,
          calendarId: 'primary',
          suggestion: 'This event is very similar to an existing one. Is this intentional?'
        },
        {
          event: {
            id: 'daily-standup',
            title: 'Daily Team Standup',
            similarity: 0.82
          },
          fullEvent: dup2,
          calendarId: '[email protected]',
          suggestion: 'This event is very similar to an existing one. Is this intentional?'
        }
      ],
      conflicts: []
    };

    const formatted = formatConflictWarnings(conflicts);
    
    // Should have two duplicate sections
    expect(formatted.match(/━━━ Duplicate Event/g)).toHaveLength(2);
    
    // First duplicate
    expect(formatted).toContain('75% similar');
    expect(formatted).toContain('Event: Team Standup');
    expect(formatted).toContain('Event ID: morning-standup');
    
    // Second duplicate
    expect(formatted).toContain('82% similar');
    expect(formatted).toContain('Event: Daily Team Standup');
    expect(formatted).toContain('Description: Quick sync');
    expect(formatted).toContain('Location: Zoom');
  });

  it('should handle duplicates without full event details gracefully', () => {
    const conflicts: ConflictCheckResult = {
      hasConflicts: true,
      duplicates: [{
        event: {
          id: 'basic-dup',
          title: 'Meeting',
          url: 'https://calendar.google.com/event/basic-dup',
          similarity: 0.7
        },
        suggestion: 'This event is very similar to an existing one. Is this intentional?'
      }],
      conflicts: []
    };

    const formatted = formatConflictWarnings(conflicts);
    
    expect(formatted).toContain('70% similar');
    expect(formatted).toContain('"Meeting"');
    expect(formatted).toContain('View existing event: https://calendar.google.com/event/basic-dup');
    expect(formatted).not.toContain('Existing event details:');
  });
});
```

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

```typescript
import { calendar_v3 } from "googleapis";

export class EventSimilarityChecker {
  private readonly DEFAULT_SIMILARITY_THRESHOLD = 0.7;

  /**
   * Check if two events are potentially duplicates based on similarity
   * Uses simplified rules-based approach instead of complex weighted calculations
   */
  checkSimilarity(event1: calendar_v3.Schema$Event, event2: calendar_v3.Schema$Event): number {
    // Check if one is all-day and the other is timed
    const event1IsAllDay = this.isAllDayEvent(event1);
    const event2IsAllDay = this.isAllDayEvent(event2);
    
    if (event1IsAllDay !== event2IsAllDay) {
      // Different event types - not duplicates
      return 0.2; // Low similarity
    }
    
    const titleMatch = this.titlesMatch(event1.summary, event2.summary);
    const timeOverlap = this.eventsOverlap(event1, event2);
    const sameDay = this.eventsOnSameDay(event1, event2);
    
    // Simple rules-based scoring
    if (titleMatch.exact && timeOverlap) {
      return 0.95; // Almost certainly a duplicate
    }
    
    if (titleMatch.similar && timeOverlap) {
      return 0.7; // Potential duplicate
    }
    
    if (titleMatch.exact && sameDay) {
      return 0.6; // Same title on same day but different times
    }
    
    if (titleMatch.exact && !sameDay) {
      return 0.4; // Same title but different day - likely recurring event
    }
    
    if (titleMatch.similar) {
      return 0.3; // Similar titles only
    }
    
    return 0.1; // No significant similarity
  }

  /**
   * Check if an event is an all-day event
   */
  private isAllDayEvent(event: calendar_v3.Schema$Event): boolean {
    return !event.start?.dateTime && !!event.start?.date;
  }

  /**
   * Check if two titles match (exact or similar)
   * Simplified string matching without Levenshtein distance
   */
  private titlesMatch(title1?: string | null, title2?: string | null): { exact: boolean; similar: boolean } {
    if (!title1 || !title2) {
      return { exact: false, similar: false };
    }
    
    const t1 = title1.toLowerCase().trim();
    const t2 = title2.toLowerCase().trim();
    
    // Exact match
    if (t1 === t2) {
      return { exact: true, similar: true };
    }
    
    // Check if one contains the other (for variations like "Meeting" vs "Team Meeting")
    if (t1.includes(t2) || t2.includes(t1)) {
      return { exact: false, similar: true };
    }
    
    // Check for common significant words (more than 3 characters)
    const words1 = t1.split(/\s+/).filter(w => w.length > 3);
    const words2 = t2.split(/\s+/).filter(w => w.length > 3);
    
    if (words1.length > 0 && words2.length > 0) {
      const commonWords = words1.filter(w => words2.includes(w));
      const similarity = commonWords.length / Math.min(words1.length, words2.length);
      
      return { exact: false, similar: similarity >= 0.5 };
    }
    
    return { exact: false, similar: false };
  }

  /**
   * Check if two events are on the same day
   */
  private eventsOnSameDay(event1: calendar_v3.Schema$Event, event2: calendar_v3.Schema$Event): boolean {
    const time1 = this.getEventTime(event1);
    const time2 = this.getEventTime(event2);
    
    if (!time1 || !time2) return false;
    
    // Compare dates only (ignore time)
    const date1 = new Date(time1.start);
    const date2 = new Date(time2.start);
    
    return date1.getFullYear() === date2.getFullYear() &&
           date1.getMonth() === date2.getMonth() &&
           date1.getDate() === date2.getDate();
  }

  /**
   * Extract event time information
   * 
   * Note: This method handles both:
   * - Events being created (may have timezone-naive datetimes with separate timeZone field)
   * - Events from Google Calendar (have timezone-aware datetimes)
   * 
   * The MCP trusts Google Calendar to return only relevant events in the queried time range.
   * Any timezone conversions are handled by the Google Calendar API, not by this service.
   */
  private getEventTime(event: calendar_v3.Schema$Event): { start: Date; end: Date } | null {
    const startTime = event.start?.dateTime || event.start?.date;
    const endTime = event.end?.dateTime || event.end?.date;
    
    if (!startTime || !endTime) return null;
    
    // Parse the datetime strings as-is
    // Google Calendar API ensures we only get events in the requested time range
    return {
      start: new Date(startTime),
      end: new Date(endTime)
    };
  }

  /**
   * Check if two events overlap in time
   * Consolidated overlap logic used throughout the service
   */
  eventsOverlap(event1: calendar_v3.Schema$Event, event2: calendar_v3.Schema$Event): boolean {
    const time1 = this.getEventTime(event1);
    const time2 = this.getEventTime(event2);
    
    if (!time1 || !time2) return false;
    
    return time1.start < time2.end && time2.start < time1.end;
  }

  /**
   * Calculate overlap duration in milliseconds
   * Used by ConflictAnalyzer for detailed overlap analysis
   */
  calculateOverlapDuration(event1: calendar_v3.Schema$Event, event2: calendar_v3.Schema$Event): number {
    const time1 = this.getEventTime(event1);
    const time2 = this.getEventTime(event2);
    
    if (!time1 || !time2) return 0;
    
    const overlapStart = Math.max(time1.start.getTime(), time2.start.getTime());
    const overlapEnd = Math.min(time1.end.getTime(), time2.end.getTime());
    return Math.max(0, overlapEnd - overlapStart);
  }

  /**
   * Determine if events are likely duplicates
   */
  isDuplicate(event1: calendar_v3.Schema$Event, event2: calendar_v3.Schema$Event, threshold?: number): boolean {
    const similarity = this.checkSimilarity(event1, event2);
    return similarity >= (threshold || this.DEFAULT_SIMILARITY_THRESHOLD);
  }
}
```

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

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GetEventHandler } from '../../../handlers/core/GetEventHandler.js';
import { OAuth2Client } from 'google-auth-library';

// Mock the googleapis module
vi.mock('googleapis', () => ({
  google: {
    calendar: vi.fn(() => ({
      events: {
        get: vi.fn()
      }
    }))
  },
  calendar_v3: {}
}));

// Mock the field mask builder
vi.mock('../../../utils/field-mask-builder.js', () => ({
  buildSingleEventFieldMask: vi.fn((fields) => {
    if (!fields || fields.length === 0) return undefined;
    return fields.join(',');
  })
}));

describe('GetEventHandler', () => {
  let handler: GetEventHandler;
  let mockOAuth2Client: OAuth2Client;
  let mockCalendar: any;

  beforeEach(() => {
    handler = new GetEventHandler();
    mockOAuth2Client = new OAuth2Client();
    
    // Setup mock calendar
    mockCalendar = {
      events: {
        get: vi.fn()
      }
    };
    
    // Mock the getCalendar method
    vi.spyOn(handler as any, 'getCalendar').mockReturnValue(mockCalendar);
  });

  describe('runTool', () => {
    it('should retrieve an event successfully', async () => {
      const mockEvent = {
        id: 'event123',
        summary: 'Test Event',
        start: { dateTime: '2025-01-15T10:00:00Z' },
        end: { dateTime: '2025-01-15T11:00:00Z' },
        status: 'confirmed'
      };

      mockCalendar.events.get.mockResolvedValue({ data: mockEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123'
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.get).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123'
      });

      expect(result.content[0].type).toBe('text');
      const response = JSON.parse(result.content[0].text);
      expect(response.event).toBeDefined();
      expect(response.event.id).toBe('event123');
      expect(response.event.summary).toBe('Test Event');
    });

    it('should retrieve an event with custom fields', async () => {
      const mockEvent = {
        id: 'event123',
        summary: 'Test Event',
        start: { dateTime: '2025-01-15T10:00:00Z' },
        end: { dateTime: '2025-01-15T11:00:00Z' },
        description: 'Event description',
        colorId: '5',
        attendees: [{ email: '[email protected]' }]
      };

      mockCalendar.events.get.mockResolvedValue({ data: mockEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        fields: ['description', 'colorId', 'attendees']
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.get).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        fields: 'description,colorId,attendees'
      });

      const response = JSON.parse(result.content[0].text);
      expect(response.event).toBeDefined();
      expect(response.event.id).toBe('event123');
      expect(response.event.description).toBe('Event description');
      expect(response.event.colorId).toBe('5');
    });

    it('should handle event not found', async () => {
      const notFoundError = new Error('Not found');
      (notFoundError as any).code = 404;
      mockCalendar.events.get.mockRejectedValue(notFoundError);

      const args = {
        calendarId: 'primary',
        eventId: 'nonexistent'
      };

      // Now throws an error instead of returning a message
      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow(
        "Event with ID 'nonexistent' not found in calendar 'primary'."
      );
    });

    it('should handle API errors', async () => {
      const apiError = new Error('API Error');
      (apiError as any).code = 500;
      mockCalendar.events.get.mockRejectedValue(apiError);

      const args = {
        calendarId: 'primary',
        eventId: 'event123'
      };

      // Mock handleGoogleApiError to throw a specific error
      vi.spyOn(handler as any, 'handleGoogleApiError').mockImplementation(() => {
        throw new Error('Handled API Error');
      });

      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow('Handled API Error');
    });

    it('should handle null event response', async () => {
      mockCalendar.events.get.mockResolvedValue({ data: null });

      const args = {
        calendarId: 'primary',
        eventId: 'event123'
      };

      // Now throws an error instead of returning a message
      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow(
        "Event with ID 'event123' not found in calendar 'primary'."
      );
    });
  });

  describe('field mask integration', () => {
    it('should not include fields parameter when no fields requested', async () => {
      const mockEvent = {
        id: 'event123',
        summary: 'Test Event'
      };

      mockCalendar.events.get.mockResolvedValue({ data: mockEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123'
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.get).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123'
      });
    });

    it('should include fields parameter when fields are requested', async () => {
      const mockEvent = {
        id: 'event123',
        summary: 'Test Event',
        description: 'Test Description'
      };

      mockCalendar.events.get.mockResolvedValue({ data: mockEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        fields: ['description']
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.get).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        fields: 'description'
      });
    });
  });
});
```

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

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
import { OAuth2Client } from "google-auth-library";

// Import authentication components
import { initializeOAuth2Client } from './auth/client.js';
import { AuthServer } from './auth/server.js';
import { TokenManager } from './auth/tokenManager.js';

// Import tool registry
import { ToolRegistry } from './tools/registry.js';

// Import transport handlers
import { StdioTransportHandler } from './transports/stdio.js';
import { HttpTransportHandler, HttpTransportConfig } from './transports/http.js';

// Import config
import { ServerConfig } from './config/TransportConfig.js';

export class GoogleCalendarMcpServer {
  private server: McpServer;
  private oauth2Client!: OAuth2Client;
  private tokenManager!: TokenManager;
  private authServer!: AuthServer;
  private config: ServerConfig;

  constructor(config: ServerConfig) {
    this.config = config;
    this.server = new McpServer({
      name: "google-calendar",
      version: "1.3.0"
    });
  }

  async initialize(): Promise<void> {
    // 1. Initialize Authentication (but don't block on it)
    this.oauth2Client = await initializeOAuth2Client();
    this.tokenManager = new TokenManager(this.oauth2Client);
    this.authServer = new AuthServer(this.oauth2Client);

    // 2. Handle startup authentication based on transport type
    await this.handleStartupAuthentication();

    // 3. Set up Modern Tool Definitions
    this.registerTools();

    // 4. Set up Graceful Shutdown
    this.setupGracefulShutdown();
  }

  private async handleStartupAuthentication(): Promise<void> {
    // Skip authentication in test environment
    if (process.env.NODE_ENV === 'test') {
      return;
    }
    
    const accountMode = this.tokenManager.getAccountMode();
    
    if (this.config.transport.type === 'stdio') {
      // For stdio mode, ensure authentication before starting server
      const hasValidTokens = await this.tokenManager.validateTokens(accountMode);
      if (!hasValidTokens) {
        // Ensure we're using the correct account mode (don't override it)
        const authSuccess = await this.authServer.start(true); // openBrowser = true
        if (!authSuccess) {
          process.stderr.write(`Authentication failed for ${accountMode} account. Please check your OAuth credentials and try again.\n`);
          process.exit(1);
        }
        process.stderr.write(`Successfully authenticated user.\n`);
      } else {
        process.stderr.write(`Valid ${accountMode} user tokens found, skipping authentication prompt.\n`);
      }
    } else {
      // For HTTP mode, check for tokens but don't block startup
      const hasValidTokens = await this.tokenManager.validateTokens(accountMode);
      if (!hasValidTokens) {
        process.stderr.write(`⚠️  No valid ${accountMode} user authentication tokens found.\n`);
        process.stderr.write('Visit the server URL in your browser to authenticate, or run "npm run auth" separately.\n');
      } else {
        process.stderr.write(`Valid ${accountMode} user tokens found.\n`);
      }
    }
  }

  private registerTools(): void {
    ToolRegistry.registerAll(this.server, this.executeWithHandler.bind(this));
  }

  private async ensureAuthenticated(): Promise<void> {
    // Check if we already have valid tokens
    if (await this.tokenManager.validateTokens()) {
      return;
    }

    // For stdio mode, authentication should have been handled at startup
    if (this.config.transport.type === 'stdio') {
      throw new McpError(
        ErrorCode.InvalidRequest,
        "Authentication tokens are no longer valid. Please restart the server to re-authenticate."
      );
    }

    // For HTTP mode, try to start auth server if not already running
    try {
      const authSuccess = await this.authServer.start(false); // openBrowser = false for HTTP mode
      
      if (!authSuccess) {
        throw new McpError(
          ErrorCode.InvalidRequest,
          "Authentication required. Please run 'npm run auth' to authenticate, or visit the auth URL shown in the logs for HTTP mode."
        );
      }
    } catch (error) {
      if (error instanceof McpError) {
        throw error;
      }
      if (error instanceof Error) {
        throw new McpError(ErrorCode.InvalidRequest, error.message);
      }
      throw new McpError(ErrorCode.InvalidRequest, "Authentication required. Please run 'npm run auth' to authenticate.");
    }
  }

  private async executeWithHandler(handler: any, args: any): Promise<{ content: Array<{ type: "text"; text: string }> }> {
    await this.ensureAuthenticated();
    const result = await handler.runTool(args, this.oauth2Client);
    return result;
  }

  async start(): Promise<void> {
    switch (this.config.transport.type) {
      case 'stdio':
        const stdioHandler = new StdioTransportHandler(this.server);
        await stdioHandler.connect();
        break;
        
      case 'http':
        const httpConfig: HttpTransportConfig = {
          port: this.config.transport.port,
          host: this.config.transport.host
        };
        const httpHandler = new HttpTransportHandler(this.server, httpConfig);
        await httpHandler.connect();
        break;
        
      default:
        throw new Error(`Unsupported transport type: ${this.config.transport.type}`);
    }
  }

  private setupGracefulShutdown(): void {
    const cleanup = async () => {
      try {
        if (this.authServer) {
          await this.authServer.stop();
        }
        
        // McpServer handles transport cleanup automatically
        this.server.close();
        
        process.exit(0);
      } catch (error: unknown) {
        process.stderr.write(`Error during cleanup: ${error instanceof Error ? error.message : error}\n`);
        process.exit(1);
      }
    };

    process.on("SIGINT", cleanup);
    process.on("SIGTERM", cleanup);
  }

  // Expose server for testing
  getServer(): McpServer {
    return this.server;
  }
} 
```

--------------------------------------------------------------------------------
/src/tests/unit/utils/event-id-validator.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from 'vitest';
import {
  isValidEventId,
  validateEventId,
  sanitizeEventId
} from '../../../utils/event-id-validator.js';

describe('Event ID Validator', () => {
  describe('isValidEventId', () => {
    it('should accept valid event IDs', () => {
      expect(isValidEventId('abcdef123456')).toBe(true);
      expect(isValidEventId('event2025')).toBe(true);
      expect(isValidEventId('a1b2c3d4e5')).toBe(true);
      expect(isValidEventId('meeting0115')).toBe(true);
      expect(isValidEventId('12345')).toBe(true); // Minimum length
      expect(isValidEventId('abcdefghijklmnopqrstuv0123456789')).toBe(true); // All valid chars
    });

    it('should reject IDs that are too short', () => {
      expect(isValidEventId('')).toBe(false);
      expect(isValidEventId('a')).toBe(false);
      expect(isValidEventId('ab')).toBe(false);
      expect(isValidEventId('abc')).toBe(false);
      expect(isValidEventId('abcd')).toBe(false); // 4 chars, min is 5
    });

    it('should reject IDs that are too long', () => {
      const longId = 'a'.repeat(1025);
      expect(isValidEventId(longId)).toBe(false);
    });

    it('should accept IDs at boundary lengths', () => {
      const minId = 'a'.repeat(5);
      const maxId = 'a'.repeat(1024);
      expect(isValidEventId(minId)).toBe(true);
      expect(isValidEventId(maxId)).toBe(true);
    });

    it('should reject IDs with invalid characters', () => {
      expect(isValidEventId('event id')).toBe(false); // Space
      expect(isValidEventId('event_id')).toBe(false); // Underscore
      expect(isValidEventId('event.id')).toBe(false); // Period
      expect(isValidEventId('event/id')).toBe(false); // Slash
      expect(isValidEventId('event@id')).toBe(false); // At symbol
      expect(isValidEventId('event#id')).toBe(false); // Hash
      expect(isValidEventId('event$id')).toBe(false); // Dollar
      expect(isValidEventId('event%id')).toBe(false); // Percent
      expect(isValidEventId('event-id')).toBe(false); // Hyphen (not allowed in base32hex)
      expect(isValidEventId('EventID')).toBe(false); // Uppercase (not allowed)
      expect(isValidEventId('eventwxyz')).toBe(false); // Letters w,x,y,z not in base32hex
    });
  });

  describe('validateEventId', () => {
    it('should not throw for valid event IDs', () => {
      expect(() => validateEventId('validevent123')).not.toThrow();
      expect(() => validateEventId('event2025')).not.toThrow();
      expect(() => validateEventId('abcdefghijklmnopqrstuv')).not.toThrow();
    });

    it('should throw with specific error for short IDs', () => {
      expect(() => validateEventId('abc')).toThrow('Invalid event ID: must be at least 5 characters long');
    });

    it('should throw with specific error for long IDs', () => {
      const longId = 'a'.repeat(1025);
      expect(() => validateEventId(longId)).toThrow('Invalid event ID: must not exceed 1024 characters');
    });

    it('should throw with specific error for invalid characters', () => {
      expect(() => validateEventId('event_id_123')).toThrow('Invalid event ID: can only contain lowercase letters a-v and digits 0-9 (base32hex encoding)');
      expect(() => validateEventId('event-id')).toThrow('Invalid event ID: can only contain lowercase letters a-v and digits 0-9 (base32hex encoding)');
      expect(() => validateEventId('EventID')).toThrow('Invalid event ID: can only contain lowercase letters a-v and digits 0-9 (base32hex encoding)');
    });

    it('should combine multiple error messages', () => {
      expect(() => validateEventId('a b')).toThrow('Invalid event ID: must be at least 5 characters long, can only contain lowercase letters a-v and digits 0-9 (base32hex encoding)');
    });
  });

  describe('sanitizeEventId', () => {
    it('should convert to valid base32hex characters', () => {
      expect(sanitizeEventId('event id 123')).toMatch(/^[a-v0-9]+$/);
      expect(sanitizeEventId('event_id_123')).toMatch(/^[a-v0-9]+$/);
      expect(sanitizeEventId('event.id.123')).toMatch(/^[a-v0-9]+$/);
      // Check specific conversions
      expect(sanitizeEventId('eventid123')).toBe('eventid123');
      expect(sanitizeEventId('EventID123')).toBe('eventid123'); // Lowercase
      expect(sanitizeEventId('event-id-123')).toMatch(/^eventid123/); // Remove hyphens
    });

    it('should map w-z to a-d', () => {
      expect(sanitizeEventId('wxyz')).toMatch(/^abcd/);
      // 'event_with_xyz' -> 'eventaithbcd' (underscores removed, w in 'with' -> a, then xyz -> bcd)
      expect(sanitizeEventId('event_with_xyz')).toBe('eventaithbcd');
    });

    it('should handle special characters', () => {
      expect(sanitizeEventId('-event-id-')).toMatch(/^eventid/);
      expect(sanitizeEventId('___event___')).toMatch(/^event/);
    });

    it('should pad short IDs to meet minimum length', () => {
      const result = sanitizeEventId('ab');
      expect(result.length).toBeGreaterThanOrEqual(5);
      expect(result).toMatch(/^ab[a-v0-9]+$/); // Should append valid base32hex chars
    });

    it('should truncate long IDs to maximum length', () => {
      const longInput = 'a'.repeat(2000);
      const result = sanitizeEventId(longInput);
      expect(result.length).toBe(1024);
    });

    it('should handle empty input', () => {
      const result = sanitizeEventId('');
      expect(result).toMatch(/^event[a-v0-9]+$/);
      expect(result.length).toBeGreaterThanOrEqual(5);
    });

    it('should handle input with only invalid characters', () => {
      const result = sanitizeEventId('!@#$%');
      expect(result).toMatch(/^ev[a-v0-9]+$/);
    });

    it('should preserve valid characters', () => {
      const result = sanitizeEventId('validevent123');
      expect(result).toBe('validevent123');
      // But convert uppercase to lowercase
      const result2 = sanitizeEventId('ValidEvent123');
      expect(result2).toBe('validevent123');
    });

    it('should handle mixed valid and invalid characters', () => {
      const result = sanitizeEventId('Event!@#2025$%^Meeting');
      expect(result).toMatch(/^event2025meeting/);
      expect(result).toMatch(/^[a-v0-9]+$/);
    });
  });
});
```

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

```typescript
import { describe, it, expect } from 'vitest';
import { formatConflictWarnings } from '../../../handlers/utils.js';
import { ConflictCheckResult } from '../../../services/conflict-detection/types.js';
import { calendar_v3 } from 'googleapis';

describe('Enhanced Conflict Response Formatting', () => {
  it('should format duplicate warnings with full event details', () => {
    const fullEvent: calendar_v3.Schema$Event = {
      id: 'duplicate123',
      summary: 'Team Meeting',
      description: 'Weekly team sync',
      location: 'Conference Room A',
      start: { dateTime: '2024-01-15T10:00:00Z' },
      end: { dateTime: '2024-01-15T11:00:00Z' },
      attendees: [
        { email: '[email protected]', displayName: 'John Doe', responseStatus: 'accepted' }
      ],
      htmlLink: 'https://calendar.google.com/event?eid=duplicate123'
    };

    const conflicts: ConflictCheckResult = {
      hasConflicts: true,
      duplicates: [{
        event: {
          id: 'duplicate123',
          title: 'Team Meeting',
          url: 'https://calendar.google.com/event?eid=duplicate123',
          similarity: 0.95
        },
        fullEvent: fullEvent,
        suggestion: 'This appears to be a duplicate. Consider updating the existing event instead.'
      }],
      conflicts: []
    };

    const formatted = formatConflictWarnings(conflicts);
    
    expect(formatted).toContain('POTENTIAL DUPLICATES DETECTED');
    expect(formatted).toContain('95% similar');
    expect(formatted).toContain('Existing event details:');
    expect(formatted).toContain('Event: Team Meeting');
    expect(formatted).toContain('Event ID: duplicate123');
    expect(formatted).toContain('Description: Weekly team sync');
    expect(formatted).toContain('Location: Conference Room A');
    expect(formatted).toContain('John Doe (accepted)');
    expect(formatted).toContain('View: https://calendar.google.com/event?eid=duplicate123');
  });

  it('should format conflict warnings with full event details', () => {
    const conflictingEvent: calendar_v3.Schema$Event = {
      id: 'conflict456',
      summary: 'Design Review',
      description: 'Q4 design review meeting',
      location: 'Room 201',
      start: { dateTime: '2024-01-15T13:30:00Z' },
      end: { dateTime: '2024-01-15T14:30:00Z' },
      htmlLink: 'https://calendar.google.com/event?eid=conflict456'
    };

    const conflicts: ConflictCheckResult = {
      hasConflicts: true,
      duplicates: [],
      conflicts: [{
        type: 'overlap',
        calendar: 'primary',
        event: {
          id: 'conflict456',
          title: 'Design Review',
          url: 'https://calendar.google.com/event?eid=conflict456',
          start: '2024-01-15T13:30:00Z',
          end: '2024-01-15T14:30:00Z'
        },
        fullEvent: conflictingEvent,
        overlap: {
          duration: '30 minutes',
          percentage: 50,
          startTime: '2024-01-15T13:30:00Z',
          endTime: '2024-01-15T14:00:00Z'
        }
      }]
    };

    const formatted = formatConflictWarnings(conflicts);
    
    expect(formatted).toContain('SCHEDULING CONFLICTS DETECTED');
    expect(formatted).toContain('Calendar: primary');
    expect(formatted).toContain('Conflicting Event');
    expect(formatted).toContain('Overlap: 30 minutes (50% of your event)');
    expect(formatted).toContain('Conflicting event details:');
    expect(formatted).toContain('Event: Design Review');
    expect(formatted).toContain('Description: Q4 design review meeting');
    expect(formatted).toContain('Location: Room 201');
  });

  it('should fallback gracefully when full event details are not available', () => {
    const conflicts: ConflictCheckResult = {
      hasConflicts: true,
      duplicates: [{
        event: {
          id: 'dup789',
          title: 'Standup',
          url: 'https://calendar.google.com/event?eid=dup789',
          similarity: 0.85
        },
        suggestion: 'This event is very similar to an existing one. Is this intentional?'
      }],
      conflicts: []
    };

    const formatted = formatConflictWarnings(conflicts);
    
    expect(formatted).toContain('POTENTIAL DUPLICATES DETECTED');
    expect(formatted).toContain('85% similar');
    expect(formatted).toContain('"Standup"');
    expect(formatted).toContain('View existing event: https://calendar.google.com/event?eid=dup789');
    expect(formatted).not.toContain('Existing event details:'); // Should not show this section
  });

  it('should format multiple conflicts with proper separation', () => {
    const conflicts: ConflictCheckResult = {
      hasConflicts: true,
      duplicates: [],
      conflicts: [
        {
          type: 'overlap',
          calendar: '[email protected]',
          event: {
            id: 'work1',
            title: 'Sprint Planning',
            url: 'https://calendar.google.com/event?eid=work1'
          },
          fullEvent: {
            id: 'work1',
            summary: 'Sprint Planning',
            start: { dateTime: '2024-01-15T09:00:00Z' },
            end: { dateTime: '2024-01-15T10:00:00Z' }
          },
          overlap: {
            duration: '15 minutes',
            percentage: 25,
            startTime: '2024-01-15T09:45:00Z',
            endTime: '2024-01-15T10:00:00Z'
          }
        },
        {
          type: 'overlap',
          calendar: '[email protected]',
          event: {
            id: 'work2',
            title: 'Daily Standup',
            url: 'https://calendar.google.com/event?eid=work2'
          },
          overlap: {
            duration: '30 minutes',
            percentage: 100,
            startTime: '2024-01-15T10:00:00Z',
            endTime: '2024-01-15T10:30:00Z'
          }
        }
      ]
    };

    const formatted = formatConflictWarnings(conflicts);
    
    expect(formatted).toContain('Calendar: [email protected]');
    expect(formatted.match(/━━━ Conflicting Event ━━━/g)).toHaveLength(2);
    expect(formatted).toContain('Sprint Planning');
    expect(formatted).toContain('Daily Standup');
    expect(formatted).toContain('15 minutes (25% of your event)');
    expect(formatted).toContain('30 minutes (100% of your event)');
  });
});
```

--------------------------------------------------------------------------------
/src/tests/unit/services/conflict-detection/ConflictAnalyzer.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from 'vitest';
import { ConflictAnalyzer } from '../../../../services/conflict-detection/ConflictAnalyzer.js';
import { calendar_v3 } from 'googleapis';

describe('ConflictAnalyzer', () => {
  const analyzer = new ConflictAnalyzer();

  describe('analyzeOverlap', () => {
    it('should detect full overlap', () => {
      const event1: calendar_v3.Schema$Event = {
        summary: 'Meeting 1',
        start: { dateTime: '2024-01-01T10:00:00Z' },
        end: { dateTime: '2024-01-01T11:00:00Z' }
      };
      const event2: calendar_v3.Schema$Event = {
        summary: 'Meeting 2',
        start: { dateTime: '2024-01-01T10:00:00Z' },
        end: { dateTime: '2024-01-01T11:00:00Z' }
      };

      const result = analyzer.analyzeOverlap(event1, event2);
      expect(result.hasOverlap).toBe(true);
      expect(result.percentage).toBe(100);
      expect(result.duration).toBe('1 hour');
    });

    it('should detect partial overlap', () => {
      const event1: calendar_v3.Schema$Event = {
        summary: 'Meeting 1',
        start: { dateTime: '2024-01-01T10:00:00Z' },
        end: { dateTime: '2024-01-01T11:00:00Z' }
      };
      const event2: calendar_v3.Schema$Event = {
        summary: 'Meeting 2',
        start: { dateTime: '2024-01-01T10:30:00Z' },
        end: { dateTime: '2024-01-01T11:30:00Z' }
      };

      const result = analyzer.analyzeOverlap(event1, event2);
      expect(result.hasOverlap).toBe(true);
      expect(result.percentage).toBe(50);
      expect(result.duration).toBe('30 minutes');
    });

    it('should detect no overlap', () => {
      const event1: calendar_v3.Schema$Event = {
        summary: 'Meeting 1',
        start: { dateTime: '2024-01-01T10:00:00Z' },
        end: { dateTime: '2024-01-01T11:00:00Z' }
      };
      const event2: calendar_v3.Schema$Event = {
        summary: 'Meeting 2',
        start: { dateTime: '2024-01-01T11:00:00Z' },
        end: { dateTime: '2024-01-01T12:00:00Z' }
      };

      const result = analyzer.analyzeOverlap(event1, event2);
      expect(result.hasOverlap).toBe(false);
    });

    it('should handle all-day events', () => {
      const event1: calendar_v3.Schema$Event = {
        summary: 'Conference Day 1',
        start: { date: '2024-01-01' },
        end: { date: '2024-01-02' }
      };
      const event2: calendar_v3.Schema$Event = {
        summary: 'Workshop',
        start: { dateTime: '2024-01-01T14:00:00Z' },
        end: { dateTime: '2024-01-01T16:00:00Z' }
      };

      const result = analyzer.analyzeOverlap(event1, event2);
      expect(result.hasOverlap).toBe(true);
    });

    it('should format long durations correctly', () => {
      const event1: calendar_v3.Schema$Event = {
        summary: 'Multi-day event',
        start: { dateTime: '2024-01-01T10:00:00Z' },
        end: { dateTime: '2024-01-03T14:00:00Z' }
      };
      const event2: calendar_v3.Schema$Event = {
        summary: 'Another multi-day event',
        start: { dateTime: '2024-01-02T08:00:00Z' },
        end: { dateTime: '2024-01-04T12:00:00Z' }
      };

      const result = analyzer.analyzeOverlap(event1, event2);
      expect(result.hasOverlap).toBe(true);
      expect(result.duration).toContain('day');
    });
  });

  describe('findOverlappingEvents', () => {
    it('should find all overlapping events', () => {
      const targetEvent: calendar_v3.Schema$Event = {
        id: 'target',
        summary: 'Target Event',
        start: { dateTime: '2024-01-01T10:00:00Z' },
        end: { dateTime: '2024-01-01T12:00:00Z' }
      };

      const events: calendar_v3.Schema$Event[] = [
        {
          id: '1',
          summary: 'Before - No overlap',
          start: { dateTime: '2024-01-01T08:00:00Z' },
          end: { dateTime: '2024-01-01T09:00:00Z' }
        },
        {
          id: '2',
          summary: 'Partial overlap at start',
          start: { dateTime: '2024-01-01T09:30:00Z' },
          end: { dateTime: '2024-01-01T10:30:00Z' }
        },
        {
          id: '3',
          summary: 'Full overlap',
          start: { dateTime: '2024-01-01T10:00:00Z' },
          end: { dateTime: '2024-01-01T12:00:00Z' }
        },
        {
          id: '4',
          summary: 'Partial overlap at end',
          start: { dateTime: '2024-01-01T11:30:00Z' },
          end: { dateTime: '2024-01-01T13:00:00Z' }
        },
        {
          id: '5',
          summary: 'After - No overlap',
          start: { dateTime: '2024-01-01T13:00:00Z' },
          end: { dateTime: '2024-01-01T14:00:00Z' }
        },
        {
          id: 'target',
          summary: 'Same event (should be skipped)',
          start: { dateTime: '2024-01-01T10:00:00Z' },
          end: { dateTime: '2024-01-01T12:00:00Z' }
        },
        {
          id: '6',
          summary: 'Cancelled event',
          status: 'cancelled',
          start: { dateTime: '2024-01-01T10:30:00Z' },
          end: { dateTime: '2024-01-01T11:30:00Z' }
        }
      ];

      const overlapping = analyzer.findOverlappingEvents(events, targetEvent);
      expect(overlapping).toHaveLength(3);
      expect(overlapping.map(e => e.id)).toEqual(['2', '3', '4']);
    });

    it('should handle empty event list', () => {
      const targetEvent: calendar_v3.Schema$Event = {
        summary: 'Target Event',
        start: { dateTime: '2024-01-01T10:00:00Z' },
        end: { dateTime: '2024-01-01T12:00:00Z' }
      };

      const overlapping = analyzer.findOverlappingEvents([], targetEvent);
      expect(overlapping).toHaveLength(0);
    });

    it('should handle events without time information', () => {
      const targetEvent: calendar_v3.Schema$Event = {
        summary: 'Target Event',
        start: { dateTime: '2024-01-01T10:00:00Z' },
        end: { dateTime: '2024-01-01T12:00:00Z' }
      };

      const events: calendar_v3.Schema$Event[] = [
        {
          id: '1',
          summary: 'Event without time'
        },
        {
          id: '2',
          summary: 'Valid event',
          start: { dateTime: '2024-01-01T11:00:00Z' },
          end: { dateTime: '2024-01-01T13:00:00Z' }
        }
      ];

      const overlapping = analyzer.findOverlappingEvents(events, targetEvent);
      expect(overlapping).toHaveLength(1);
      expect(overlapping[0].id).toBe('2');
    });
  });
});
```
Page 1/5FirstPrevNextLast