#
tokens: 49701/50000 23/104 files (page 2/5)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 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

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

```typescript
import { describe, it, expect, vi } from 'vitest';
import { GetCurrentTimeHandler } from '../../../handlers/core/GetCurrentTimeHandler.js';
import { OAuth2Client } from 'google-auth-library';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';

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

  describe('runTool', () => {
    it('should return current time without timezone parameter using primary calendar timezone', async () => {
      const handler = new GetCurrentTimeHandler();
      // Mock calendar timezone to avoid real API calls in unit tests
      const spy = vi.spyOn(GetCurrentTimeHandler.prototype as any, 'getCalendarTimezone').mockResolvedValue('America/Los_Angeles');
      const result = await handler.runTool({}, mockOAuth2Client);
      
      expect(result.content).toHaveLength(1);
      expect(result.content[0].type).toBe('text');
      
      const response = JSON.parse(result.content[0].text as string);
      expect(response.currentTime).toBeDefined();
      expect(response.currentTime).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
      expect(response.timezone).toBe('America/Los_Angeles');
      expect(response.offset).toBeDefined();
      expect(response.isDST).toBeTypeOf('boolean');
      spy.mockRestore();
    });

    it('should return current time with valid timezone parameter', async () => {
      const handler = new GetCurrentTimeHandler();
      const result = await handler.runTool({ timeZone: 'America/New_York' }, mockOAuth2Client);
      
      expect(result.content).toHaveLength(1);
      expect(result.content[0].type).toBe('text');
      
      const response = JSON.parse(result.content[0].text as string);
      expect(response.currentTime).toBeDefined();
      expect(response.currentTime).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
      expect(response.timezone).toBe('America/New_York');
      expect(response.offset).toBeDefined();
      expect(response.isDST).toBeTypeOf('boolean');
    });

    it('should handle UTC timezone parameter', async () => {
      const handler = new GetCurrentTimeHandler();
      const result = await handler.runTool({ timeZone: 'UTC' }, mockOAuth2Client);
      
      const response = JSON.parse(result.content[0].text as string);
      expect(response.timezone).toBe('UTC');
      expect(response.offset).toBe('Z');
      expect(response.isDST).toBe(false);
    });

    it('should throw error for invalid timezone', async () => {
      const handler = new GetCurrentTimeHandler();
      
      await expect(handler.runTool({ timeZone: 'Invalid/Timezone' }, mockOAuth2Client))
        .rejects.toThrow(McpError);

      try {
        await handler.runTool({ timeZone: 'Invalid/Timezone' }, mockOAuth2Client);
      } catch (error) {
        expect(error).toBeInstanceOf(McpError);
        expect((error as McpError).code).toBe(ErrorCode.InvalidRequest);
        expect((error as McpError).message).toContain('Invalid timezone');
        expect((error as McpError).message).toContain('Invalid/Timezone');
      }
    });
  });

  describe('timezone validation', () => {
    it('should validate common IANA timezones', async () => {
      const handler = new GetCurrentTimeHandler();
      const validTimezones = [
        'UTC',
        'America/Los_Angeles',
        'America/New_York',
        'Europe/London',
        'Asia/Tokyo',
        'Australia/Sydney'
      ];

      for (const timezone of validTimezones) {
        const result = await handler.runTool({ timeZone: timezone }, mockOAuth2Client);
        const response = JSON.parse(result.content[0].text as string);
        expect(response.timezone).toBe(timezone);
      }
    });

    it('should reject invalid timezone formats', async () => {
      const handler = new GetCurrentTimeHandler();
      const invalidTimezones = [
        'Pacific/Invalid',
        'Not/A/Timezone',
        'Invalid/Timezone',
        'XYZ',
        'foo/bar'
      ];

      for (const timezone of invalidTimezones) {
        await expect(handler.runTool({ timeZone: timezone }, mockOAuth2Client))
          .rejects.toThrow(McpError);
      }
    });
  });

  describe('output format', () => {
    it('should include all required fields in response without timezone', async () => {
      const handler = new GetCurrentTimeHandler();
      const spy = vi.spyOn(GetCurrentTimeHandler.prototype as any, 'getCalendarTimezone').mockResolvedValue('America/Los_Angeles');
      const result = await handler.runTool({}, mockOAuth2Client);
      const response = JSON.parse(result.content[0].text as string);

      expect(response).toHaveProperty('currentTime');
      expect(response).toHaveProperty('timezone');
      expect(response).toHaveProperty('offset');
      expect(response).toHaveProperty('isDST');
      expect(response.currentTime).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
      expect(response.timezone).toBe('America/Los_Angeles');
      expect(response.isDST).toBeTypeOf('boolean');
      spy.mockRestore();
    });

    it('should include all required fields in response with timezone', async () => {
      const handler = new GetCurrentTimeHandler();
      const result = await handler.runTool({ timeZone: 'UTC' }, mockOAuth2Client);
      const response = JSON.parse(result.content[0].text as string);

      expect(response).toHaveProperty('currentTime');
      expect(response).toHaveProperty('timezone');
      expect(response).toHaveProperty('offset');
      expect(response).toHaveProperty('isDST');
      expect(response.currentTime).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
      expect(response.timezone).toBe('UTC');
      expect(response.offset).toBe('Z');
      expect(response.isDST).toBe(false);
    });

    it('should format RFC3339 timestamps correctly', async () => {
      const handler = new GetCurrentTimeHandler();
      const result = await handler.runTool({ timeZone: 'UTC' }, mockOAuth2Client);
      const response = JSON.parse(result.content[0].text as string);

      // Should match ISO8601 pattern
      expect(response.currentTime).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
      // Offset should be in the format +HH:MM, -HH:MM, or Z
      expect(response.offset).toMatch(/^([+-]\d{2}:\d{2}|Z)$/);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/tests/unit/console-statements.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Test to ensure no console.log statements exist in the source code
 * This helps maintain MCP compliance since console statements are not supported in MCP clients
 */
import { describe, it, expect } from 'vitest';
import { readFileSync, readdirSync, statSync } from 'fs';
import { join, extname } from 'path';

// Files and directories that are allowed to have console statements
const ALLOWED_CONSOLE_FILES = [
  // Example files are allowed to have console statements
  'examples/',
  // Build scripts are allowed to have console statements  
  'scripts/',
  // Test files can have console statements for debugging
  '.test.ts',
  '.test.js',
  // Documentation files
  '.md',
  // Config files
  'vitest.config.ts',
  'tsconfig.json'
];

// Console methods that should not be present in source code
const FORBIDDEN_CONSOLE_METHODS = [
  'console.log',
  'console.warn',
  'console.info',
  'console.debug',
  'console.trace',
  'console.time',
  'console.timeEnd',
  'console.table',
  'console.assert',
  'console.clear',
  'console.count',
  'console.countReset',
  'console.dir',
  'console.dirxml',
  'console.group',
  'console.groupCollapsed',
  'console.groupEnd'
];

interface ConsoleFinding {
  file: string;
  line: number;
  content: string;
  method: string;
}

/**
 * Recursively get all TypeScript and JavaScript files in a directory
 */
function getSourceFiles(dir: string, basePath: string = ''): string[] {
  const files: string[] = [];
  const items = readdirSync(dir);

  for (const item of items) {
    const fullPath = join(dir, item);
    const relativePath = join(basePath, item);
    const stat = statSync(fullPath);

    if (stat.isDirectory()) {
      // Skip node_modules and other non-source directories
      if (!['node_modules', '.git', 'build', 'dist', 'coverage'].includes(item)) {
        files.push(...getSourceFiles(fullPath, relativePath));
      }
    } else if (stat.isFile()) {
      const ext = extname(item);
      if (['.ts', '.js', '.mjs', '.cjs'].includes(ext)) {
        files.push(relativePath);
      }
    }
  }

  return files;
}

/**
 * Check if a file should be excluded from console statement checking
 */
function isFileAllowed(filePath: string): boolean {
  return ALLOWED_CONSOLE_FILES.some(allowedPath => 
    filePath.includes(allowedPath) || filePath.endsWith(allowedPath)
  );
}

/**
 * Find console statements in a file
 */
function findConsoleStatements(filePath: string, content: string): ConsoleFinding[] {
  const findings: ConsoleFinding[] = [];
  const lines = content.split('\n');

  lines.forEach((line, index) => {
    const trimmedLine = line.trim();
    
    // Skip comments
    if (trimmedLine.startsWith('//') || trimmedLine.startsWith('*') || trimmedLine.startsWith('/*')) {
      return;
    }

    // Check for console statements
    for (const method of FORBIDDEN_CONSOLE_METHODS) {
      if (line.includes(method)) {
        // Make sure it's not in a string literal or comment
        const methodIndex = line.indexOf(method);
        const beforeMethod = line.substring(0, methodIndex);
        
        // Simple check to avoid false positives in strings
        const singleQuotes = (beforeMethod.match(/'/g) || []).length;
        const doubleQuotes = (beforeMethod.match(/"/g) || []).length;
        const backticks = (beforeMethod.match(/`/g) || []).length;
        
        // If we're inside quotes, skip this match
        if (singleQuotes % 2 === 1 || doubleQuotes % 2 === 1 || backticks % 2 === 1) {
          continue;
        }

        findings.push({
          file: filePath,
          line: index + 1,
          content: line.trim(),
          method
        });
      }
    }
  });

  return findings;
}

describe('Console Statement Detection', () => {
  it('should not contain any console.log statements in source code', () => {
    const sourceFiles = getSourceFiles('./src');
    const allFindings: ConsoleFinding[] = [];

    for (const file of sourceFiles) {
      // Skip files that are allowed to have console statements
      if (isFileAllowed(file)) {
        continue;
      }

      try {
        const fullPath = join('./src', file);
        const content = readFileSync(fullPath, 'utf-8');
        const findings = findConsoleStatements(file, content);
        allFindings.push(...findings);
      } catch (error) {
        // Skip files that can't be read
        continue;
      }
    }

    // Create a detailed error message if console statements are found
    if (allFindings.length > 0) {
      const errorMessage = [
        `Found ${allFindings.length} console statement(s) in source code:`,
        '',
        ...allFindings.map(finding => 
          `  ${finding.file}:${finding.line} - ${finding.method} in: ${finding.content}`
        ),
        '',
        'Console statements are not supported in MCP clients.',
        'Use process.stderr.write() for error logging instead.',
        'For debugging, consider using a proper logging library or remove before committing.'
      ].join('\n');

      expect(allFindings).toEqual([]);
      throw new Error(errorMessage);
    }

    expect(allFindings).toEqual([]);
  });

  it('should properly detect console statements in test content', () => {
    // Test the detection logic itself with some sample content
    const testContent = `
      function test() {
        console.log("This should be detected");
        const x = "console.log in string should be ignored";
        // console.warn("This is a comment, should be ignored");
        console.warn("This should be detected");
        console.error("This is now allowed and should not be detected");
        process.stderr.write("This is allowed\\n");
      }
    `;

    const findings = findConsoleStatements('test-file.ts', testContent);
    
    expect(findings).toHaveLength(2);
    expect(findings[0].method).toBe('console.log');
    expect(findings[1].method).toBe('console.warn');
  });

  it('should ignore console statements in strings and comments', () => {
    const testContent = `
      // This console.log is in a comment
      const message = "Use console.log for debugging";
      const template = \`Don't use console.error here\`;
      /* console.warn in block comment */
    `;

    const findings = findConsoleStatements('test-file.ts', testContent);
    
    // Should not find any console statements since they're all in comments or strings
    expect(findings).toHaveLength(0);
  });
}); 
```

--------------------------------------------------------------------------------
/src/utils/response-builder.ts:
--------------------------------------------------------------------------------

```typescript
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { ConflictCheckResult } from "../services/conflict-detection/types.js";
import {
  ConflictInfo,
  DuplicateInfo,
  convertGoogleEventToStructured,
  StructuredEvent
} from "../types/structured-responses.js";
import { calendar_v3 } from "googleapis";

/**
 * Creates a structured JSON response for MCP tools
 *
 * Note: We use compact JSON (no pretty-printing) because MCP clients
 * are expected to parse and display the JSON themselves. Pretty-printing
 * with escaped newlines (\n) creates poor display in clients that show
 * the raw text.
 */
export function createStructuredResponse<T>(data: T): CallToolResult {
  return {
    content: [{
      type: "text",
      text: JSON.stringify(data)
    }]
  };
}

/**
 * Converts conflict check results to structured format
 */
export function convertConflictsToStructured(
  conflicts: ConflictCheckResult
): { conflicts?: ConflictInfo[]; duplicates?: DuplicateInfo[] } {
  const result: { conflicts?: ConflictInfo[]; duplicates?: DuplicateInfo[] } = {};
  
  if (conflicts.duplicates.length > 0) {
    result.duplicates = conflicts.duplicates.map(dup => {
      // Get start and end from fullEvent if available
      let start = '';
      let end = '';
      if (dup.fullEvent) {
        start = dup.fullEvent.start?.dateTime || dup.fullEvent.start?.date || '';
        end = dup.fullEvent.end?.dateTime || dup.fullEvent.end?.date || '';
      }
      
      return {
        event: {
          id: dup.event.id || '',
          title: dup.event.title,
          start,
          end,
          url: dup.event.url,
          similarity: dup.event.similarity
        },
        calendarId: dup.calendarId || '',
        suggestion: dup.suggestion
      };
    });
  }
  
  if (conflicts.conflicts.length > 0) {
    result.conflicts = conflicts.conflicts.map(conflict => {
      // Get start and end from either the event object or fullEvent
      let start = conflict.event.start || '';
      let end = conflict.event.end || '';
      if (!start && conflict.fullEvent) {
        start = conflict.fullEvent.start?.dateTime || conflict.fullEvent.start?.date || '';
      }
      if (!end && conflict.fullEvent) {
        end = conflict.fullEvent.end?.dateTime || conflict.fullEvent.end?.date || '';
      }
      
      return {
        event: {
          id: conflict.event.id || '',
          title: conflict.event.title,
          start,
          end,
          url: conflict.event.url,
          similarity: conflict.similarity
        },
        calendar: conflict.calendar,
        overlap: conflict.overlap ? {
          duration: conflict.overlap.duration,
          percentage: `${conflict.overlap.percentage}%`
        } : undefined
      };
    });
  }
  
  return result;
}

/**
 * Converts an array of Google Calendar events to structured format
 */
export function convertEventsToStructured(
  events: calendar_v3.Schema$Event[],
  calendarId?: string
): StructuredEvent[] {
  return events.map(event => convertGoogleEventToStructured(event, calendarId));
}

/**
 * Helper to add calendar ID to events
 */
export function addCalendarIdToEvents(
  events: calendar_v3.Schema$Event[],
  calendarId: string
): StructuredEvent[] {
  return events.map(event => ({
    ...convertGoogleEventToStructured(event),
    calendarId
  }));
}

/**
 * Formats free/busy information into structured format
 */
export function formatFreeBusyStructured(
  freeBusy: any,
  timeMin: string,
  timeMax: string
): {
  timeMin: string;
  timeMax: string;
  calendars: Record<string, {
    busy: Array<{ start: string; end: string }>;
    errors?: Array<{ domain?: string; reason?: string }>;
  }>;
} {
  const calendars: Record<string, any> = {};
  
  if (freeBusy.calendars) {
    for (const [calId, calData] of Object.entries(freeBusy.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;
      }
    }
  }
  
  return {
    timeMin,
    timeMax,
    calendars
  };
}

/**
 * Converts calendar list to structured format
 */
export function convertCalendarsToStructured(
  calendars: calendar_v3.Schema$CalendarListEntry[]
): Array<{
  id: string;
  summary?: string;
  description?: string;
  location?: string;
  timeZone?: string;
  summaryOverride?: string;
  colorId?: string;
  backgroundColor?: string;
  foregroundColor?: string;
  hidden?: boolean;
  selected?: boolean;
  accessRole?: string;
  defaultReminders?: Array<{ method: 'email' | 'popup'; minutes: number }>;
  notificationSettings?: {
    notifications?: Array<{ type?: string; method?: string }>;
  };
  primary?: boolean;
  deleted?: boolean;
  conferenceProperties?: {
    allowedConferenceSolutionTypes?: string[];
  };
}> {
  return 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
  }));
}

/**
 * Creates a warning message for conflicts/duplicates
 */
export function createWarningsArray(conflicts?: ConflictCheckResult): string[] | undefined {
  if (!conflicts || !conflicts.hasConflicts) {
    return undefined;
  }
  
  const warnings: string[] = [];
  
  if (conflicts.duplicates.length > 0) {
    warnings.push(`Found ${conflicts.duplicates.length} potential duplicate(s)`);
  }
  
  if (conflicts.conflicts.length > 0) {
    warnings.push(`Found ${conflicts.conflicts.length} scheduling conflict(s)`);
  }
  
  return warnings.length > 0 ? warnings : undefined;
}
```

--------------------------------------------------------------------------------
/scripts/dev.js:
--------------------------------------------------------------------------------

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

/**
 * Development and Advanced Features Helper
 * Provides access to advanced scripts without cluttering package.json
 */

import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import path from 'path';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.resolve(__dirname, '..');

// Command definitions
const commands = {
  // HTTP Transport
  'http': {
    description: 'Start HTTP server on localhost:3000',
    cmd: 'node',
    args: ['build/index.js', '--transport', 'http', '--port', '3000']
  },
  'http:public': {
    description: 'Start HTTP server accessible from any host',
    cmd: 'node', 
    args: ['build/index.js', '--transport', 'http', '--port', '3000', '--host', '0.0.0.0']
  },

  // Authentication Management
  'auth': {
    description: 'Manually authenticate normal account',
    cmd: 'node',
    args: ['build/auth-server.js'],
    env: { GOOGLE_ACCOUNT_MODE: 'normal' }
  },
  'auth:test': {
    description: 'Authenticate test account',
    cmd: 'node',
    args: ['build/auth-server.js'],
    env: { GOOGLE_ACCOUNT_MODE: 'test' }
  },
  'account:status': {
    description: 'Check account status and configuration',
    cmd: 'node',
    args: ['scripts/account-manager.js', 'status']
  },
  'account:clear:normal': {
    description: 'Clear normal account tokens',
    cmd: 'node',
    args: ['scripts/account-manager.js', 'clear', 'normal']
  },
  'account:clear:test': {
    description: 'Clear test account tokens',
    cmd: 'node',
    args: ['scripts/account-manager.js', 'clear', 'test']
  },

  // Unit Testing
  'test': {
    description: 'Run unit tests (excludes integration tests)',
    cmd: 'npm',
    args: ['test']
  },
  'test:integration:direct': {
    description: 'Run core integration tests (recommended for development)',
    cmd: 'npx',
    args: ['vitest', 'run', 'src/tests/integration/direct-integration.test.ts']
  },
  'test:integration:claude': {
    description: 'Run Claude + MCP integration tests',
    cmd: 'npx',
    args: ['vitest', 'run', 'src/tests/integration/claude-mcp-integration.test.ts']
  },
  'test:integration:openai': {
    description: 'Run OpenAI + MCP integration tests',
    cmd: 'npx',
    args: ['vitest', 'run', 'src/tests/integration/openai-mcp-integration.test.ts']
  },
  'test:integration:all': {
    description: 'Run complete integration test suite',
    cmd: 'npm',
    args: ['run', 'test:integration']
  },
  'test:watch:all': {
    description: 'Run all tests in watch mode',
    cmd: 'npx',
    args: ['vitest']
  },

  // Coverage and Analysis
  'coverage': {
    description: 'Generate test coverage report',
    cmd: 'npm',
    args: ['run', 'test:coverage']
  },

  // Docker Operations
  'docker:build': {
    description: 'Build Docker image',
    cmd: 'docker',
    args: ['compose', 'build']
  },
  'docker:up': {
    description: 'Start Docker container (stdio mode)',
    cmd: 'docker',
    args: ['compose', 'up']
  },
  'docker:up:http': {
    description: 'Start Docker container in HTTP mode (edit docker-compose.yml first)',
    cmd: 'docker',
    args: ['compose', 'up']
  },
  'docker:down': {
    description: 'Stop and remove Docker container',
    cmd: 'docker',
    args: ['compose', 'down']
  },
  'docker:logs': {
    description: 'View Docker container logs',
    cmd: 'docker',
    args: ['compose', 'logs', '-f']
  },
  'docker:exec': {
    description: 'Execute commands in running Docker container',
    cmd: 'docker',
    args: ['compose', 'exec', 'calendar-mcp', 'sh']
  },
  'docker:auth': {
    description: 'Authenticate OAuth in Docker container',
    cmd: 'docker',
    args: ['compose', 'exec', 'calendar-mcp', 'npm', 'run', 'auth']
  },
  'docker:test:quick': {
    description: 'Run quick Docker tests (no OAuth required)',
    cmd: 'bash',
    args: ['scripts/test-docker.sh', '--quick']
  },

};

function showHelp() {
  console.log('🛠️  Google Calendar MCP - Development Helper\n');
  console.log('Usage: npm run dev <command>\n');
  console.log('Available commands:\n');

  const categories = {
    'HTTP Transport': ['http', 'http:public'],
    'Authentication': ['auth', 'auth:test', 'account:status', 'account:clear:normal', 'account:clear:test'],
    'Unit Testing': ['test'],
    'Integration Testing': ['test:integration:direct', 'test:integration:claude', 'test:integration:openai', 'test:integration:all', 'test:watch:all'],
    'Docker Operations': ['docker:build', 'docker:up', 'docker:up:http', 'docker:auth', 'docker:logs', 'docker:down', 'docker:exec', 'docker:test:quick'],
    'Coverage & Analysis': ['coverage']
  };

  for (const [category, cmdList] of Object.entries(categories)) {
    console.log(`\x1b[1m${category}:\x1b[0m`);
    for (const cmd of cmdList) {
      if (commands[cmd]) {
        console.log(`  ${cmd.padEnd(25)} ${commands[cmd].description}`);
      }
    }
    console.log('');
  }

  console.log('Examples:');
  console.log('  npm run dev http                  # Start HTTP server');
  console.log('  npm run dev docker:up             # Start Docker container');
  console.log('  npm run dev docker:auth           # Authenticate in Docker');
  console.log('  npm run dev test:integration:direct # Run core integration tests');
}

async function runCommand(commandName) {
  const command = commands[commandName];
  if (!command) {
    console.error(`❌ Unknown command: ${commandName}`);
    console.log('\nRun "npm run dev" to see available commands.');
    process.exit(1);
  }

  // Handle preBuild flag
  if (command.preBuild) {
    console.log(`📦 Building project first...`);
    await new Promise((resolve, reject) => {
      const buildChild = spawn('npm', ['run', 'build'], {
        stdio: 'inherit',
        cwd: rootDir
      });
      
      buildChild.on('exit', (code) => {
        if (code !== 0) {
          reject(new Error(`Build failed with exit code ${code}`));
        } else {
          resolve();
        }
      });
      
      buildChild.on('error', reject);
    }).catch(error => {
      console.error(`\n❌ Build failed: ${error.message}`);
      process.exit(1);
    });
    console.log(`✅ Build complete\n`);
  }

  console.log(`🚀 Running: ${commandName}`);
  console.log(`   Command: ${command.cmd} ${command.args.join(' ')}\n`);

  const env = { 
    ...process.env, 
    ...(command.env || {}) 
  };

  const child = spawn(command.cmd, command.args, {
    stdio: 'inherit',
    cwd: rootDir,
    env
  });

  child.on('exit', (code) => {
    if (code !== 0) {
      console.error(`\n❌ Command failed with exit code ${code}`);
      process.exit(code);
    }
  });

  child.on('error', (error) => {
    console.error(`❌ Failed to run command: ${error.message}`);
    process.exit(1);
  });
}

// Main execution
const [,, commandName] = process.argv;

if (!commandName || commandName === 'help' || commandName === '--help') {
  showHelp();
} else {
  runCommand(commandName);
}
```

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

```typescript
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { OAuth2Client } from "google-auth-library";
import { CreateEventInput } from "../../tools/registry.js";
import { BaseToolHandler } from "./BaseToolHandler.js";
import { calendar_v3 } from 'googleapis';
import { createTimeObject } from "../utils/datetime.js";
import { validateEventId } from "../../utils/event-id-validator.js";
import { ConflictDetectionService } from "../../services/conflict-detection/index.js";
import { CONFLICT_DETECTION_CONFIG } from "../../services/conflict-detection/config.js";
import { createStructuredResponse, convertConflictsToStructured, createWarningsArray } from "../../utils/response-builder.js";
import { CreateEventResponse, convertGoogleEventToStructured, DuplicateInfo } from "../../types/structured-responses.js";

export class CreateEventHandler extends BaseToolHandler {
    private conflictDetectionService: ConflictDetectionService;
    
    constructor() {
        super();
        this.conflictDetectionService = new ConflictDetectionService();
    }
    
    async runTool(args: any, oauth2Client: OAuth2Client): Promise<CallToolResult> {
        const validArgs = args as CreateEventInput;

        // Create the event object for conflict checking
        const timezone = args.timeZone || await this.getCalendarTimezone(oauth2Client, validArgs.calendarId);
        const eventToCheck: calendar_v3.Schema$Event = {
            summary: args.summary,
            description: args.description,
            start: createTimeObject(args.start, timezone),
            end: createTimeObject(args.end, timezone),
            attendees: args.attendees,
            location: args.location,
        };
        
        // Check for conflicts and duplicates
        const conflicts = await this.conflictDetectionService.checkConflicts(
            oauth2Client,
            eventToCheck,
            validArgs.calendarId,
            {
                checkDuplicates: true,
                checkConflicts: true,
                calendarsToCheck: validArgs.calendarsToCheck || [validArgs.calendarId],
                duplicateSimilarityThreshold: validArgs.duplicateSimilarityThreshold || CONFLICT_DETECTION_CONFIG.DEFAULT_DUPLICATE_THRESHOLD
            }
        );
        
        // Block creation if exact or near-exact duplicate found
        const exactDuplicate = conflicts.duplicates.find(
            dup => dup.event.similarity >= CONFLICT_DETECTION_CONFIG.DUPLICATE_THRESHOLDS.BLOCKING
        );
        
        if (exactDuplicate && validArgs.allowDuplicates !== true) {
            // Create a duplicate error response
            const duplicateInfo: DuplicateInfo = {
                event: {
                    id: exactDuplicate.event.id || '',
                    title: exactDuplicate.event.title,
                    start: exactDuplicate.event.start || '',
                    end: exactDuplicate.event.end || '',
                    url: exactDuplicate.event.url,
                    similarity: exactDuplicate.event.similarity
                },
                calendarId: exactDuplicate.calendarId || '',
                suggestion: exactDuplicate.suggestion
            };
            
            // Throw an error that will be handled by MCP SDK
            throw new Error(
                `Duplicate event detected (${Math.round(exactDuplicate.event.similarity * 100)}% similar). ` +
                `Event "${exactDuplicate.event.title}" already exists. ` +
                `To create anyway, set allowDuplicates to true.`
            );
        }
        
        // Create the event
        const event = await this.createEvent(oauth2Client, validArgs);
        
        // Generate structured response with conflict warnings
        const structuredConflicts = convertConflictsToStructured(conflicts);
        const response: CreateEventResponse = {
            event: convertGoogleEventToStructured(event, validArgs.calendarId),
            conflicts: structuredConflicts.conflicts,
            duplicates: structuredConflicts.duplicates,
            warnings: createWarningsArray(conflicts)
        };
        
        return createStructuredResponse(response);
    }

    private async createEvent(
        client: OAuth2Client,
        args: CreateEventInput
    ): Promise<calendar_v3.Schema$Event> {
        try {
            const calendar = this.getCalendar(client);
            
            // Validate custom event ID if provided
            if (args.eventId) {
                validateEventId(args.eventId);
            }
            
            // Use provided timezone or calendar's default timezone
            const timezone = args.timeZone || await this.getCalendarTimezone(client, args.calendarId);
            
            const requestBody: calendar_v3.Schema$Event = {
                summary: args.summary,
                description: args.description,
                start: createTimeObject(args.start, timezone),
                end: createTimeObject(args.end, timezone),
                attendees: args.attendees,
                location: args.location,
                colorId: args.colorId,
                reminders: args.reminders,
                recurrence: args.recurrence,
                transparency: args.transparency,
                visibility: args.visibility,
                guestsCanInviteOthers: args.guestsCanInviteOthers,
                guestsCanModify: args.guestsCanModify,
                guestsCanSeeOtherGuests: args.guestsCanSeeOtherGuests,
                anyoneCanAddSelf: args.anyoneCanAddSelf,
                conferenceData: args.conferenceData,
                extendedProperties: args.extendedProperties,
                attachments: args.attachments,
                source: args.source,
                ...(args.eventId && { id: args.eventId }) // Include custom ID if provided
            };
            
            // Determine if we need to enable conference data or attachments
            const conferenceDataVersion = args.conferenceData ? 1 : undefined;
            const supportsAttachments = args.attachments ? true : undefined;
            
            const response = await calendar.events.insert({
                calendarId: args.calendarId,
                requestBody: requestBody,
                sendUpdates: args.sendUpdates,
                ...(conferenceDataVersion && { conferenceDataVersion }),
                ...(supportsAttachments && { supportsAttachments })
            });
            
            if (!response.data) throw new Error('Failed to create event, no data returned');
            return response.data;
        } catch (error: any) {
            // Handle ID conflict errors specifically
            if (error?.code === 409 || error?.response?.status === 409) {
                throw new Error(`Event ID '${args.eventId}' already exists. Please use a different ID.`);
            }
            throw this.handleGoogleApiError(error);
        }
    }
}

```

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

```typescript
import { calendar_v3 } from 'googleapis';
import { createTimeObject } from '../utils/datetime.js';

export class RecurringEventHelpers {
  private calendar: calendar_v3.Calendar;

  constructor(calendar: calendar_v3.Calendar) {
    this.calendar = calendar;
  }

  /**
   * Get the calendar instance
   */
  getCalendar(): calendar_v3.Calendar {
    return this.calendar;
  }

  /**
   * Detects if an event is recurring or single
   */
  async detectEventType(eventId: string, calendarId: string): Promise<'recurring' | 'single'> {
    const response = await this.calendar.events.get({
      calendarId,
      eventId
    });

    const event = response.data;
    return event.recurrence && event.recurrence.length > 0 ? 'recurring' : 'single';
  }

  /**
   * Formats an instance ID for single instance updates
   */
  formatInstanceId(eventId: string, originalStartTime: string): string {
    // Convert to UTC first, then format to basic format: YYYYMMDDTHHMMSSZ
    const utcDate = new Date(originalStartTime);
    const basicTimeFormat = utcDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
    
    return `${eventId}_${basicTimeFormat}`;
  }

  /**
   * Calculates the UNTIL date for future instance updates
   */
  calculateUntilDate(futureStartDate: string): string {
    const futureDate = new Date(futureStartDate);
    const untilDate = new Date(futureDate.getTime() - 86400000); // -1 day
    return untilDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
  }

  /**
   * Calculates end time based on original duration
   */
  calculateEndTime(newStartTime: string, originalEvent: calendar_v3.Schema$Event): string {
    const newStart = new Date(newStartTime);
    const originalStart = new Date(originalEvent.start!.dateTime!);
    const originalEnd = new Date(originalEvent.end!.dateTime!);
    const duration = originalEnd.getTime() - originalStart.getTime();
    
    return new Date(newStart.getTime() + duration).toISOString();
  }

  /**
   * Updates recurrence rule with UNTIL clause
   */
  updateRecurrenceWithUntil(recurrence: string[], untilDate: string): string[] {
    if (!recurrence || recurrence.length === 0) {
      throw new Error('No recurrence rule found');
    }

    const updatedRecurrence: string[] = [];
    let foundRRule = false;

    for (const rule of recurrence) {
      if (rule.startsWith('RRULE:')) {
        foundRRule = true;
        const updatedRule = rule
          .replace(/;UNTIL=\d{8}T\d{6}Z/g, '') // Remove existing UNTIL
          .replace(/;COUNT=\d+/g, '') // Remove COUNT if present
          + `;UNTIL=${untilDate}`;
        updatedRecurrence.push(updatedRule);
      } else {
        // Preserve EXDATE, RDATE, and other rules as-is
        updatedRecurrence.push(rule);
      }
    }

    if (!foundRRule) {
      throw new Error('No RRULE found in recurrence rules');
    }

    return updatedRecurrence;
  }

  /**
   * Cleans event fields for new event creation
   */
  cleanEventForDuplication(event: calendar_v3.Schema$Event): calendar_v3.Schema$Event {
    const cleanedEvent = { ...event };
    
    // Remove fields that shouldn't be duplicated
    delete cleanedEvent.id;
    delete cleanedEvent.etag;
    delete cleanedEvent.iCalUID;
    delete cleanedEvent.created;
    delete cleanedEvent.updated;
    delete cleanedEvent.htmlLink;
    delete cleanedEvent.hangoutLink;
    
    return cleanedEvent;
  }

  /**
   * Builds request body for event updates
   */
  buildUpdateRequestBody(args: any, defaultTimeZone?: string): calendar_v3.Schema$Event {
    const requestBody: calendar_v3.Schema$Event = {};

    if (args.summary !== undefined && args.summary !== null) requestBody.summary = args.summary;
    if (args.description !== undefined && args.description !== null) requestBody.description = args.description;
    if (args.location !== undefined && args.location !== null) requestBody.location = args.location;
    if (args.colorId !== undefined && args.colorId !== null) requestBody.colorId = args.colorId;
    if (args.attendees !== undefined && args.attendees !== null) requestBody.attendees = args.attendees;
    if (args.reminders !== undefined && args.reminders !== null) requestBody.reminders = args.reminders;
    if (args.recurrence !== undefined && args.recurrence !== null) requestBody.recurrence = args.recurrence;
    if (args.conferenceData !== undefined && args.conferenceData !== null) requestBody.conferenceData = args.conferenceData;
    if (args.transparency !== undefined && args.transparency !== null) requestBody.transparency = args.transparency;
    if (args.visibility !== undefined && args.visibility !== null) requestBody.visibility = args.visibility;
    if (args.guestsCanInviteOthers !== undefined && args.guestsCanInviteOthers !== null) requestBody.guestsCanInviteOthers = args.guestsCanInviteOthers;
    if (args.guestsCanModify !== undefined && args.guestsCanModify !== null) requestBody.guestsCanModify = args.guestsCanModify;
    if (args.guestsCanSeeOtherGuests !== undefined && args.guestsCanSeeOtherGuests !== null) requestBody.guestsCanSeeOtherGuests = args.guestsCanSeeOtherGuests;
    if (args.anyoneCanAddSelf !== undefined && args.anyoneCanAddSelf !== null) requestBody.anyoneCanAddSelf = args.anyoneCanAddSelf;
    if (args.extendedProperties !== undefined && args.extendedProperties !== null) requestBody.extendedProperties = args.extendedProperties;
    if (args.attachments !== undefined && args.attachments !== null) requestBody.attachments = args.attachments;

    // Handle time changes - use createTimeObject to support both timed and all-day events
    const effectiveTimeZone = args.timeZone || defaultTimeZone;

    if (args.start !== undefined && args.start !== null) {
      const timeObj = createTimeObject(args.start, effectiveTimeZone);
      // When converting between formats, explicitly nullify the opposite field
      // This is required by Google Calendar API to successfully convert between timed and all-day events
      if (timeObj.date !== undefined) {
        // All-day event: set date and nullify dateTime
        requestBody.start = { date: timeObj.date, dateTime: null };
      } else {
        // Timed event: set dateTime/timeZone and nullify date
        requestBody.start = { dateTime: timeObj.dateTime, timeZone: timeObj.timeZone, date: null };
      }
    }
    if (args.end !== undefined && args.end !== null) {
      const timeObj = createTimeObject(args.end, effectiveTimeZone);
      // When converting between formats, explicitly nullify the opposite field
      if (timeObj.date !== undefined) {
        // All-day event: set date and nullify dateTime
        requestBody.end = { date: timeObj.date, dateTime: null };
      } else {
        // Timed event: set dateTime/timeZone and nullify date
        requestBody.end = { dateTime: timeObj.dateTime, timeZone: timeObj.timeZone, date: null };
      }
    }

    return requestBody;
  }
}

/**
 * Custom error class for recurring event errors
 */
export class RecurringEventError extends Error {
  public code: string;

  constructor(message: string, code: string) {
    super(message);
    this.name = 'RecurringEventError';
    this.code = code;
  }
}

export const RECURRING_EVENT_ERRORS = {
  INVALID_SCOPE: 'INVALID_MODIFICATION_SCOPE',
  MISSING_ORIGINAL_TIME: 'MISSING_ORIGINAL_START_TIME',
  MISSING_FUTURE_DATE: 'MISSING_FUTURE_START_DATE',
  PAST_FUTURE_DATE: 'FUTURE_DATE_IN_PAST',
  NON_RECURRING_SCOPE: 'SCOPE_NOT_APPLICABLE_TO_SINGLE_EVENT'
}; 
```

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

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

export class GetCurrentTimeHandler extends BaseToolHandler {
    async runTool(args: any, oauth2Client: OAuth2Client): Promise<CallToolResult> {
        // Validate arguments using schema
        const validArgs = args as GetCurrentTimeInput;
        
        const now = new Date();
        
        // If no timezone provided, use the primary Google Calendar's default timezone
        const requestedTimeZone = validArgs.timeZone;
        
        let timezone: string;

        if (requestedTimeZone) {
            // Validate the timezone
            if (!this.isValidTimeZone(requestedTimeZone)) {
                throw new McpError(
                    ErrorCode.InvalidRequest,
                    `Invalid timezone: ${requestedTimeZone}. Use IANA timezone format like 'America/Los_Angeles' or 'UTC'.`
                );
            }
            timezone = requestedTimeZone;
        } else {
            // No timezone requested - fetch the primary calendar's timezone
            // If fetching fails (e.g., auth/network), fall back to system timezone
            try {
                timezone = await this.getCalendarTimezone(oauth2Client, 'primary');
                // If we got UTC back, it might be a fallback, try to detect if it's actually the system timezone
                if (timezone === 'UTC') {
                    const systemTz = this.getSystemTimeZone();
                    if (systemTz !== 'UTC') {
                        // Likely failed to get calendar timezone
                        timezone = systemTz;
                    }
                }
            } catch (error) {
                // This shouldn't happen with current implementation, but handle it
                timezone = this.getSystemTimeZone();
            }
        }

        const response: GetCurrentTimeResponse = {
            currentTime: now.toISOString(),
            timezone: timezone,
            offset: this.getTimezoneOffset(now, timezone),
            isDST: this.isDaylightSavingTime(now, timezone)
        };

        return createStructuredResponse(response);
    }
    
    private getSystemTimeZone(): string {
        try {
            return Intl.DateTimeFormat().resolvedOptions().timeZone;
        } catch {
            return 'UTC'; // Fallback to UTC if system timezone detection fails
        }
    }
    
    private isValidTimeZone(timeZone: string): boolean {
        try {
            Intl.DateTimeFormat(undefined, { timeZone });
            return true;
        } catch {
            return false;
        }
    }
    
    private formatDateInTimeZone(date: Date, timeZone: string): string {
        const offset = this.getTimezoneOffset(date, timeZone);
        // Remove milliseconds from ISO string for proper RFC3339 format
        const isoString = date.toISOString().replace(/\.\d{3}Z$/, '');
        return isoString + offset;
    }
    
    private formatHumanReadable(date: Date, timeZone: string): string {
        const formatter = new Intl.DateTimeFormat('en-US', {
            timeZone: timeZone,
            weekday: 'long',
            year: 'numeric',
            month: 'long',
            day: 'numeric',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit',
            timeZoneName: 'long'
        });
        
        return formatter.format(date);
    }
    
    private getTimezoneOffset(_date: Date, timeZone: string): string {
        try {
            const offsetMinutes = this.getTimezoneOffsetMinutes(timeZone);
            
            if (offsetMinutes === 0) {
                return 'Z';
            }
            
            const offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);
            const offsetMins = Math.abs(offsetMinutes) % 60;
            const sign = offsetMinutes >= 0 ? '+' : '-';
            
            return `${sign}${offsetHours.toString().padStart(2, '0')}:${offsetMins.toString().padStart(2, '0')}`;
        } catch {
            return 'Z'; // Fallback to UTC if offset calculation fails
        }
    }
    
    private getTimezoneOffsetMinutes(timeZone: string): number {
        // Use the timezone offset from a date's time representation in different zones
        const date = new Date();


        // Get local time for the target timezone
        const targetTimeString = new Intl.DateTimeFormat('sv-SE', {
            timeZone: timeZone,
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit'
        }).format(date);

        // Get UTC time string
        const utcTimeString = new Intl.DateTimeFormat('sv-SE', {
            timeZone: 'UTC',
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit'
        }).format(date);

        // Parse both times and calculate difference
        const targetTime = new Date(targetTimeString.replace(' ', 'T') + 'Z').getTime();
        const utcTimeParsed = new Date(utcTimeString.replace(' ', 'T') + 'Z').getTime();

        return (targetTime - utcTimeParsed) / (1000 * 60);
    }

    private isDaylightSavingTime(date: Date, timeZone: string): boolean {
        try {
            // Get offset for the given date
            const currentOffset = this.getTimezoneOffsetForDate(date, timeZone);

            // Get offset for January 1st (typically standard time)
            const january = new Date(date.getFullYear(), 0, 1);
            const januaryOffset = this.getTimezoneOffsetForDate(january, timeZone);

            // Get offset for July 1st (typically daylight saving time if applicable)
            const july = new Date(date.getFullYear(), 6, 1);
            const julyOffset = this.getTimezoneOffsetForDate(july, timeZone);

            // If January and July have different offsets, DST is observed
            // Current date is in DST if its offset matches the smaller offset (more negative/less positive)
            if (januaryOffset !== julyOffset) {
                const dstOffset = Math.min(januaryOffset, julyOffset);
                return currentOffset === dstOffset;
            }

            return false;
        } catch {
            return false;
        }
    }

    private getTimezoneOffsetForDate(date: Date, timeZone: string): number {
        // Get local time for the target timezone
        const targetTimeString = new Intl.DateTimeFormat('sv-SE', {
            timeZone: timeZone,
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit'
        }).format(date);

        // Get UTC time string
        const utcTimeString = new Intl.DateTimeFormat('sv-SE', {
            timeZone: 'UTC',
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit'
        }).format(date);

        // Parse both times and calculate difference in minutes
        const targetTime = new Date(targetTimeString.replace(' ', 'T') + 'Z').getTime();
        const utcTimeParsed = new Date(utcTimeString.replace(' ', 'T') + 'Z').getTime();

        return (targetTime - utcTimeParsed) / (1000 * 60);
    }
}

```

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

```typescript
import { OAuth2Client } from "google-auth-library";

export interface BatchRequest {
  method: string;
  path: string;
  headers?: Record<string, string>;
  body?: any;
}

export interface BatchResponse {
  statusCode: number;
  headers: Record<string, string>;
  body: any;
  error?: any;
}

export interface BatchError {
  calendarId?: string;
  statusCode: number;
  message: string;
  details?: any;
}

export class BatchRequestError extends Error {
  constructor(
    message: string,
    public errors: BatchError[],
    public partial: boolean = false
  ) {
    super(message);
    this.name = 'BatchRequestError';
  }
}

export class BatchRequestHandler {
  private readonly batchEndpoint = "https://www.googleapis.com/batch/calendar/v3";
  private readonly boundary: string;
  private readonly maxRetries = 3;
  private readonly baseDelay = 1000; // 1 second

  constructor(private auth: OAuth2Client) {
    this.boundary = "batch_boundary_" + Date.now();
  }

  async executeBatch(requests: BatchRequest[]): Promise<BatchResponse[]> {
    if (requests.length === 0) {
      return [];
    }

    if (requests.length > 50) {
      throw new Error('Batch requests cannot exceed 50 requests per batch');
    }

    return this.executeBatchWithRetry(requests, 0);
  }

  private async executeBatchWithRetry(requests: BatchRequest[], attempt: number): Promise<BatchResponse[]> {
    try {
      const batchBody = this.createBatchBody(requests);
      const token = await this.auth.getAccessToken();
      
      const response = await fetch(this.batchEndpoint, {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${token.token}`,
          "Content-Type": `multipart/mixed; boundary=${this.boundary}`
        },
        body: batchBody
      });

      const responseText = await response.text();

      // Handle rate limiting with retry
      if (response.status === 429 && attempt < this.maxRetries) {
        const retryAfter = response.headers.get('Retry-After');
        const delay = retryAfter ? parseInt(retryAfter) * 1000 : this.baseDelay * Math.pow(2, attempt);
        
        process.stderr.write(`Rate limited, retrying after ${delay}ms (attempt ${attempt + 1}/${this.maxRetries})\n`);
        await this.sleep(delay);
        return this.executeBatchWithRetry(requests, attempt + 1);
      }

      if (!response.ok) {
        throw new BatchRequestError(
          `Batch request failed: ${response.status} ${response.statusText}`,
          [{
            statusCode: response.status,
            message: `HTTP ${response.status}: ${response.statusText}`,
            details: responseText
          }]
        );
      }

      return this.parseBatchResponse(responseText);
    } catch (error) {
      if (error instanceof BatchRequestError) {
        throw error;
      }
      
      // Retry on network errors
      if (attempt < this.maxRetries && this.isRetryableError(error)) {
        const delay = this.baseDelay * Math.pow(2, attempt);
        process.stderr.write(`Network error, retrying after ${delay}ms (attempt ${attempt + 1}/${this.maxRetries}): ${error instanceof Error ? error.message : 'Unknown error'}\n`);
        await this.sleep(delay);
        return this.executeBatchWithRetry(requests, attempt + 1);
      }
      
      // Handle network or auth errors
      throw new BatchRequestError(
        `Failed to execute batch request: ${error instanceof Error ? error.message : 'Unknown error'}`,
        [{
          statusCode: 0,
          message: error instanceof Error ? error.message : 'Unknown error',
          details: error
        }]
      );
    }
  }

  private isRetryableError(error: any): boolean {
    if (error instanceof Error) {
      const message = error.message.toLowerCase();
      return message.includes('network') || 
             message.includes('timeout') || 
             message.includes('econnreset') ||
             message.includes('enotfound');
    }
    return false;
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  private createBatchBody(requests: BatchRequest[]): string {
    return requests.map((req, index) => {
      const parts = [
        `--${this.boundary}`,
        `Content-Type: application/http`,
        `Content-ID: <item${index + 1}>`,
        "",
        `${req.method} ${req.path} HTTP/1.1`
      ];

      if (req.headers) {
        Object.entries(req.headers).forEach(([key, value]) => {
          parts.push(`${key}: ${value}`);
        });
      }

      if (req.body) {
        parts.push("Content-Type: application/json");
        parts.push("");
        parts.push(JSON.stringify(req.body));
      }

      return parts.join("\r\n");
    }).join("\r\n\r\n") + `\r\n--${this.boundary}--`;
  }

  private parseBatchResponse(responseText: string): BatchResponse[] {
    // First, try to find boundary from Content-Type header in the response
    // Google's responses typically have boundary in the first few lines
    const lines = responseText.split(/\r?\n/);
    let boundary = null;
    
    // Look for Content-Type header with boundary in the first few lines
    for (let i = 0; i < Math.min(10, lines.length); i++) {
      const line = lines[i];
      if (line.toLowerCase().includes('content-type:') && line.includes('boundary=')) {
        const boundaryMatch = line.match(/boundary=([^\s\r\n;]+)/);
        if (boundaryMatch) {
          boundary = boundaryMatch[1];
          break;
        }
      }
    }
    
    // If not found in headers, try to find boundary markers in the content
    if (!boundary) {
      const boundaryMatch = responseText.match(/--([a-zA-Z0-9_-]+)/);
      if (boundaryMatch) {
        boundary = boundaryMatch[1];
      }
    }
    
    if (!boundary) {
      throw new Error('Could not find boundary in batch response');
    }
    
    // Split by boundary markers
    const parts = responseText.split(`--${boundary}`);
    
    const responses: BatchResponse[] = [];
    
    // Skip the first part (before the first boundary) and the last part (after final boundary with --)
    for (let i = 1; i < parts.length; i++) {
      const part = parts[i];
      
      // Skip empty parts or the final boundary marker
      if (part.trim() === '' || part.trim() === '--' || part.trim().startsWith('--')) continue;
      
      const response = this.parseResponsePart(part);
      if (response) {
        responses.push(response);
      }
    }
    
    return responses;
  }

  private parseResponsePart(part: string): BatchResponse | null {
    // Handle both \r\n and \n line endings
    const lines = part.split(/\r?\n/);
    
    // Find the HTTP response line (look for "HTTP/1.1")
    let httpLineIndex = -1;
    for (let i = 0; i < lines.length; i++) {
      if (lines[i].startsWith('HTTP/1.1')) {
        httpLineIndex = i;
        break;
      }
    }

    if (httpLineIndex === -1) return null;

    // Parse status code from HTTP response line
    const httpLine = lines[httpLineIndex];
    const statusMatch = httpLine.match(/HTTP\/1\.1 (\d+)/);
    if (!statusMatch) return null;
    
    const statusCode = parseInt(statusMatch[1]);

    // Parse response headers (start after HTTP line, stop at empty line)
    const headers: Record<string, string> = {};
    let bodyStartIndex = httpLineIndex + 1;
    
    for (let i = httpLineIndex + 1; i < lines.length; i++) {
      const line = lines[i];
      if (line.trim() === '') {
        bodyStartIndex = i + 1;
        break;
      }
      
      const colonIndex = line.indexOf(':');
      if (colonIndex > 0) {
        const key = line.substring(0, colonIndex).trim();
        const value = line.substring(colonIndex + 1).trim();
        headers[key] = value;
      }
    }

    // Parse body - everything after the empty line following headers
    let body: any = null;
    if (bodyStartIndex < lines.length) {
      // Collect all body lines, filtering out empty lines at the end
      const bodyLines = [];
      for (let i = bodyStartIndex; i < lines.length; i++) {
        bodyLines.push(lines[i]);
      }
      
      // Remove trailing empty lines
      while (bodyLines.length > 0 && bodyLines[bodyLines.length - 1].trim() === '') {
        bodyLines.pop();
      }
      
      if (bodyLines.length > 0) {
        const bodyText = bodyLines.join('\n');
        if (bodyText.trim()) {
          try {
            body = JSON.parse(bodyText);
          } catch {
            // If JSON parsing fails, return the raw text
            body = bodyText;
          }
        }
      }
    }

    return {
      statusCode,
      headers,
      body
    };
  }
} 
```

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

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

describe('EventSimilarityChecker', () => {
  const checker = new EventSimilarityChecker();

  describe('checkSimilarity', () => {
    it('should return 0.95 for identical events', () => {
      const event1: calendar_v3.Schema$Event = {
        summary: 'Team Meeting',
        location: 'Conference Room A',
        start: { dateTime: '2024-01-01T10:00:00' },
        end: { dateTime: '2024-01-01T11:00:00' }
      };
      const event2 = { ...event1 };

      const similarity = checker.checkSimilarity(event1, event2);
      expect(similarity).toBe(0.95); // Our simplified algorithm returns 0.95 for exact matches
    });

    it('should detect high similarity for events with same title and time', () => {
      const event1: calendar_v3.Schema$Event = {
        summary: 'Team Meeting',
        location: 'Conference Room A',
        start: { dateTime: '2024-01-01T10:00:00' },
        end: { dateTime: '2024-01-01T11:00:00' }
      };
      const event2: calendar_v3.Schema$Event = {
        summary: 'Team Meeting',
        location: 'Conference Room B', // Different location
        start: { dateTime: '2024-01-01T10:00:00' },
        end: { dateTime: '2024-01-01T11:00:00' }
      };

      const similarity = checker.checkSimilarity(event1, event2);
      expect(similarity).toBeGreaterThan(0.8);
    });

    it('should detect moderate similarity for events with similar titles', () => {
      const event1: calendar_v3.Schema$Event = {
        summary: 'Team Meeting',
        start: { dateTime: '2024-01-01T10:00:00' },
        end: { dateTime: '2024-01-01T11:00:00' }
      };
      const event2: calendar_v3.Schema$Event = {
        summary: 'Team Meeting Discussion',
        start: { dateTime: '2024-01-01T14:00:00' }, // Different time
        end: { dateTime: '2024-01-01T15:00:00' }
      };

      const similarity = checker.checkSimilarity(event1, event2);
      expect(similarity).toBe(0.3); // Similar titles only = 0.3 in our simplified algorithm
    });

    it('should detect low similarity for completely different events', () => {
      const event1: calendar_v3.Schema$Event = {
        summary: 'Team Meeting',
        location: 'Conference Room A',
        start: { dateTime: '2024-01-01T10:00:00' },
        end: { dateTime: '2024-01-01T11:00:00' }
      };
      const event2: calendar_v3.Schema$Event = {
        summary: 'Doctor Appointment',
        location: 'Medical Center',
        start: { dateTime: '2024-02-15T09:00:00' },
        end: { dateTime: '2024-02-15T09:30:00' }
      };

      const similarity = checker.checkSimilarity(event1, event2);
      expect(similarity).toBeLessThan(0.3);
    });

    it('should handle events with missing fields', () => {
      const event1: calendar_v3.Schema$Event = {
        summary: 'Meeting',
        start: { dateTime: '2024-01-01T10:00:00' },
        end: { dateTime: '2024-01-01T11:00:00' }
      };
      const event2: calendar_v3.Schema$Event = {
        // No summary
        location: 'Room 101',
        start: { dateTime: '2024-01-01T10:00:00' },
        end: { dateTime: '2024-01-01T11:00:00' }
      };

      const similarity = checker.checkSimilarity(event1, event2);
      expect(similarity).toBeGreaterThan(0); // Time matches
      expect(similarity).toBeLessThan(0.5); // But no title match
    });

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

      const similarity = checker.checkSimilarity(event1, event2);
      expect(similarity).toBe(0.95); // Exact title + overlapping = 0.95
    });
  });

  describe('all-day vs timed events', () => {
    it('should not treat all-day event as duplicate of timed event with same title', () => {
      const allDayEvent: calendar_v3.Schema$Event = {
        summary: 'Conference',
        start: { date: '2024-01-15' },
        end: { date: '2024-01-16' }
      };
      const timedEvent: calendar_v3.Schema$Event = {
        summary: 'Conference',
        start: { dateTime: '2024-01-15T09:00:00' },
        end: { dateTime: '2024-01-15T17:00:00' }
      };

      const similarity = checker.checkSimilarity(allDayEvent, timedEvent);
      expect(similarity).toBeLessThanOrEqual(0.3);
      expect(checker.isDuplicate(allDayEvent, timedEvent)).toBe(false);
    });

    it('should not treat timed event as duplicate of all-day event', () => {
      const timedEvent: calendar_v3.Schema$Event = {
        summary: 'Team Offsite',
        location: 'Mountain View',
        start: { dateTime: '2024-01-15T10:00:00' },
        end: { dateTime: '2024-01-15T15:00:00' }
      };
      const allDayEvent: calendar_v3.Schema$Event = {
        summary: 'Team Offsite',
        location: 'Mountain View',
        start: { date: '2024-01-15' },
        end: { date: '2024-01-16' }
      };

      const similarity = checker.checkSimilarity(timedEvent, allDayEvent);
      expect(similarity).toBeLessThanOrEqual(0.3);
      expect(checker.isDuplicate(timedEvent, allDayEvent, 0.7)).toBe(false);
    });

    it('should still detect duplicates between two all-day events', () => {
      const allDay1: calendar_v3.Schema$Event = {
        summary: 'Company Holiday',
        start: { date: '2024-07-04' },
        end: { date: '2024-07-05' }
      };
      const allDay2: calendar_v3.Schema$Event = {
        summary: 'Company Holiday',
        start: { date: '2024-07-04' },
        end: { date: '2024-07-05' }
      };

      const similarity = checker.checkSimilarity(allDay1, allDay2);
      expect(similarity).toBe(0.95); // Exact title + overlapping = 0.95
      expect(checker.isDuplicate(allDay1, allDay2)).toBe(true);
    });

    it('should still detect duplicates between two timed events', () => {
      const timed1: calendar_v3.Schema$Event = {
        summary: 'Sprint Planning',
        start: { dateTime: '2024-01-15T10:00:00' },
        end: { dateTime: '2024-01-15T12:00:00' }
      };
      const timed2: calendar_v3.Schema$Event = {
        summary: 'Sprint Planning',
        start: { dateTime: '2024-01-15T10:00:00' },
        end: { dateTime: '2024-01-15T12:00:00' }
      };

      const similarity = checker.checkSimilarity(timed1, timed2);
      expect(similarity).toBe(0.95); // Exact title + overlapping = 0.95
      expect(checker.isDuplicate(timed1, timed2)).toBe(true);
    });

    it('should handle common patterns like OOO/vacation', () => {
      const allDayOOO: calendar_v3.Schema$Event = {
        summary: 'John OOO',
        start: { date: '2024-01-15' },
        end: { date: '2024-01-16' }
      };
      const timedMeeting: calendar_v3.Schema$Event = {
        summary: 'Meeting with John',
        start: { dateTime: '2024-01-15T14:00:00' },
        end: { dateTime: '2024-01-15T15:00:00' }
      };

      const similarity = checker.checkSimilarity(allDayOOO, timedMeeting);
      expect(similarity).toBeLessThan(0.3);
      expect(checker.isDuplicate(allDayOOO, timedMeeting)).toBe(false);
    });
  });

  describe('isDuplicate', () => {
    it('should identify duplicates above threshold', () => {
      const event1: calendar_v3.Schema$Event = {
        summary: 'Team Meeting',
        start: { dateTime: '2024-01-01T10:00:00' },
        end: { dateTime: '2024-01-01T11:00:00' }
      };
      const event2: calendar_v3.Schema$Event = {
        summary: 'Team Meeting',
        start: { dateTime: '2024-01-01T10:00:00' },
        end: { dateTime: '2024-01-01T11:00:00' }
      };

      expect(checker.isDuplicate(event1, event2)).toBe(true); // 0.95 >= 0.7 default threshold
      expect(checker.isDuplicate(event1, event2, 0.9)).toBe(true); // 0.95 >= 0.9
      expect(checker.isDuplicate(event1, event2, 0.96)).toBe(false); // 0.95 < 0.96
    });

    it('should not identify non-duplicates as duplicates', () => {
      const event1: calendar_v3.Schema$Event = {
        summary: 'Team Meeting',
        start: { dateTime: '2024-01-01T10:00:00' },
        end: { dateTime: '2024-01-01T11:00:00' }
      };
      const event2: calendar_v3.Schema$Event = {
        summary: 'Different Meeting',
        start: { dateTime: '2024-01-02T14:00:00' },
        end: { dateTime: '2024-01-02T15:00:00' }
      };

      expect(checker.isDuplicate(event1, event2)).toBe(false);
      expect(checker.isDuplicate(event1, event2, 0.5)).toBe(false);
    });
  });
});
```

--------------------------------------------------------------------------------
/src/tests/unit/schemas/tool-registration.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ToolRegistry, ToolSchemas } from '../../../tools/registry.js';
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

/**
 * Tool Registration Tests
 * 
 * These tests validate that all tools are properly registered with the MCP server
 * and that their schemas are correctly extracted, especially for complex schemas
 * that use .refine() methods (like update-event).
 */

describe('Tool Registration', () => {
  let mockServer: McpServer;
  let registeredTools: Array<{ name: string; description: string; inputSchema: any }>;

  beforeEach(() => {
    mockServer = new McpServer({ name: 'test', version: '1.0.0' });
    registeredTools = [];
    
    // Mock the registerTool method to capture registered tools
    mockServer.registerTool = vi.fn((name: string, definition: any, _handler: any) => {
      registeredTools.push({
        name,
        description: definition.description,
        inputSchema: definition.inputSchema
      });
      // Return a mock RegisteredTool
      return { name, description: definition.description } as any;
    });
  });

  it('should register all tools successfully without errors', async () => {
    // This should not throw any errors
    await expect(
      ToolRegistry.registerAll(mockServer, async () => ({ content: [] }))
    ).resolves.not.toThrow();
  });

  it('should register the correct number of tools', async () => {
    await ToolRegistry.registerAll(mockServer, async () => ({ content: [] }));
    
    const expectedToolCount = Object.keys(ToolSchemas).length;
    expect(registeredTools).toHaveLength(expectedToolCount);
  });

  it('should register all expected tool names', async () => {
    await ToolRegistry.registerAll(mockServer, async () => ({ content: [] }));
    
    const expectedTools = Object.keys(ToolSchemas);
    const registeredToolNames = registeredTools.map(t => t.name);
    
    for (const expectedTool of expectedTools) {
      expect(registeredToolNames).toContain(expectedTool);
    }
  });

  it('should have valid input schemas for all tools', async () => {
    await ToolRegistry.registerAll(mockServer, async () => ({ content: [] }));
    
    for (const tool of registeredTools) {
      expect(tool.inputSchema).toBeDefined();
      expect(typeof tool.inputSchema).toBe('object');
      
      // The inputSchema should be either a Zod shape object or have been converted properly
      // For tools with complex schemas, we should still get a valid object
      if (tool.name === 'update-event') {
        // This is the key test - update-event should not have an empty schema
        expect(Object.keys(tool.inputSchema).length).toBeGreaterThan(0);
      }
    }
  });

  it('should properly extract schema for update-event tool with .refine() methods', async () => {
    await ToolRegistry.registerAll(mockServer, async () => ({ content: [] }));
    
    const updateEventTool = registeredTools.find(t => t.name === 'update-event');
    expect(updateEventTool).toBeDefined();
    
    const schema = updateEventTool!.inputSchema;
    expect(schema).toBeDefined();
    
    // The key test: schema should not be empty for update-event
    expect(Object.keys(schema).length).toBeGreaterThan(0);
    
    // Check for key update-event specific properties in the Zod shape
    expect(schema).toHaveProperty('calendarId');
    expect(schema).toHaveProperty('eventId');
    expect(schema).toHaveProperty('modificationScope');
    expect(schema).toHaveProperty('originalStartTime');
    expect(schema).toHaveProperty('futureStartDate');
  });

  it('should compare update-event with create-event to ensure both have proper schemas', async () => {
    await ToolRegistry.registerAll(mockServer, async () => ({ content: [] }));
    
    const createEventTool = registeredTools.find(t => t.name === 'create-event');
    const updateEventTool = registeredTools.find(t => t.name === 'update-event');
    
    expect(createEventTool).toBeDefined();
    expect(updateEventTool).toBeDefined();
    
    // Both should have similar basic structure
    const createSchema = createEventTool!.inputSchema;
    const updateSchema = updateEventTool!.inputSchema;
    
    // Both should have non-empty schemas
    expect(Object.keys(createSchema).length).toBeGreaterThan(0);
    expect(Object.keys(updateSchema).length).toBeGreaterThan(0);
    
    // Both should have calendarId in their Zod shapes
    expect(createSchema).toHaveProperty('calendarId');
    expect(updateSchema).toHaveProperty('calendarId');
    
    // Update should have additional properties that create doesn't need
    expect(updateSchema).toHaveProperty('eventId');
    expect(updateSchema).toHaveProperty('modificationScope');
    
    // Create should have required properties that update makes optional
    expect(createSchema).toHaveProperty('summary');
    expect(createSchema).toHaveProperty('start');
    expect(createSchema).toHaveProperty('end');
  });

  it('should handle all complex schemas with refinements properly', async () => {
    await ToolRegistry.registerAll(mockServer, async () => ({ content: [] }));
    
    // Tools that use .refine() methods
    const toolsWithRefinements = ['update-event'];
    
    for (const toolName of toolsWithRefinements) {
      const tool = registeredTools.find(t => t.name === toolName);
      expect(tool).toBeDefined();
      
      const schema = tool!.inputSchema;
      expect(schema).toBeDefined();
      
      // Should not be empty - this was the original bug
      expect(Object.keys(schema).length).toBeGreaterThan(0);
    }
  });

  it('should validate that tools can be retrieved via getToolsWithSchemas()', () => {
    const tools = ToolRegistry.getToolsWithSchemas();
    
    expect(tools).toBeDefined();
    expect(Array.isArray(tools)).toBe(true);
    expect(tools.length).toBeGreaterThan(0);
    
    // Check that update-event is present and has a valid schema
    const updateEventTool = tools.find(t => t.name === 'update-event');
    expect(updateEventTool).toBeDefined();
    expect(updateEventTool!.inputSchema).toBeDefined();
    
    // The inputSchema should be a valid JSON Schema object
    expect(typeof updateEventTool!.inputSchema).toBe('object');
    expect((updateEventTool!.inputSchema as any).type).toBe('object');
  });

  it('should ensure all datetime fields have proper validation', async () => {
    await ToolRegistry.registerAll(mockServer, async () => ({ content: [] }));
    
    const toolsWithDatetime = ['create-event', 'update-event', 'list-events', 'search-events'];
    
    for (const toolName of toolsWithDatetime) {
      const tool = registeredTools.find(t => t.name === toolName);
      expect(tool).toBeDefined();
      
      const schema = tool!.inputSchema;
      expect(Object.keys(schema).length).toBeGreaterThan(0);
      
      // Just verify the schema exists and is not empty for datetime tools
      // The actual field validation is tested elsewhere
      if (toolName === 'update-event') {
        expect(schema).toHaveProperty('start');
        expect(schema).toHaveProperty('end');
      }
    }
  });

  it('should catch schema extraction issues early', async () => {
    // Test the schema extraction method directly
    const updateEventSchema = ToolSchemas['update-event'];
    expect(updateEventSchema).toBeDefined();
    
    // This should not throw an error
    const extractedShape = (ToolRegistry as any).extractSchemaShape(updateEventSchema);
    expect(extractedShape).toBeDefined();
    expect(typeof extractedShape).toBe('object');
    
    // Should have the expected properties
    expect(extractedShape).toHaveProperty('calendarId');
    expect(extractedShape).toHaveProperty('eventId');
    expect(extractedShape).toHaveProperty('modificationScope');
  });
});

/**
 * Schema Extraction Edge Cases
 * 
 * Tests to ensure the extractSchemaShape method handles various Zod schema types
 */
describe('Schema Extraction Edge Cases', () => {
  it('should handle regular ZodObject schemas', () => {
    const simpleSchema = ToolSchemas['list-calendars'];
    const extractedShape = (ToolRegistry as any).extractSchemaShape(simpleSchema);
    expect(extractedShape).toBeDefined();
  });

  it('should handle ZodEffects (refined) schemas', () => {
    const refinedSchema = ToolSchemas['update-event'];
    const extractedShape = (ToolRegistry as any).extractSchemaShape(refinedSchema);
    expect(extractedShape).toBeDefined();
    expect(typeof extractedShape).toBe('object');
  });

  it('should handle nested schema structures', () => {
    const complexSchema = ToolSchemas['create-event'];
    const extractedShape = (ToolRegistry as any).extractSchemaShape(complexSchema);
    expect(extractedShape).toBeDefined();
    expect(typeof extractedShape).toBe('object');
  });
});
```

--------------------------------------------------------------------------------
/src/handlers/core/ListEventsHandler.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 { BatchRequestHandler } from "./BatchRequestHandler.js";
import { convertToRFC3339 } from "../utils/datetime.js";
import { buildListFieldMask } from "../../utils/field-mask-builder.js";
import { createStructuredResponse } from "../../utils/response-builder.js";
import { ListEventsResponse, StructuredEvent, convertGoogleEventToStructured } from "../../types/structured-responses.js";

// Extended event type to include calendar ID for tracking source
interface ExtendedEvent extends calendar_v3.Schema$Event {
  calendarId: string;
}

interface ListEventsArgs {
  calendarId: string | string[];
  timeMin?: string;
  timeMax?: string;
  timeZone?: string;
  fields?: string[];
  privateExtendedProperty?: string[];
  sharedExtendedProperty?: string[];
}

export class ListEventsHandler extends BaseToolHandler {
    async runTool(args: ListEventsArgs, oauth2Client: OAuth2Client): Promise<CallToolResult> {
        // MCP SDK has already validated the arguments against the tool schema
        const validArgs = args;

        // Normalize calendarId to always be an array for consistent processing
        // The Zod schema transform has already handled JSON string parsing if needed
        const calendarNamesOrIds = Array.isArray(validArgs.calendarId)
            ? validArgs.calendarId
            : [validArgs.calendarId];

        // Resolve calendar names to IDs (if any names were provided)
        const calendarIds = await this.resolveCalendarIds(oauth2Client, calendarNamesOrIds);

        const allEvents = await this.fetchEvents(oauth2Client, calendarIds, {
            timeMin: validArgs.timeMin,
            timeMax: validArgs.timeMax,
            timeZone: validArgs.timeZone,
            fields: validArgs.fields,
            privateExtendedProperty: validArgs.privateExtendedProperty,
            sharedExtendedProperty: validArgs.sharedExtendedProperty
        });

        // Convert extended events to structured format
        const structuredEvents: StructuredEvent[] = allEvents.map(event =>
            convertGoogleEventToStructured(event, event.calendarId)
        );

        const response: ListEventsResponse = {
            events: structuredEvents,
            totalCount: allEvents.length,
            calendars: calendarIds.length > 1 ? calendarIds : undefined
        };

        return createStructuredResponse(response);
    }

    private async fetchEvents(
        client: OAuth2Client,
        calendarIds: string[],
        options: { timeMin?: string; timeMax?: string; timeZone?: string; fields?: string[]; privateExtendedProperty?: string[]; sharedExtendedProperty?: string[] }
    ): Promise<ExtendedEvent[]> {
        if (calendarIds.length === 1) {
            return this.fetchSingleCalendarEvents(client, calendarIds[0], options);
        }
        
        return this.fetchMultipleCalendarEvents(client, calendarIds, options);
    }

    private async fetchSingleCalendarEvents(
        client: OAuth2Client,
        calendarId: string,
        options: { timeMin?: string; timeMax?: string; timeZone?: string; fields?: string[]; privateExtendedProperty?: string[]; sharedExtendedProperty?: string[] }
    ): Promise<ExtendedEvent[]> {
        try {
            const calendar = this.getCalendar(client);
            
            // Determine timezone with correct precedence:
            // 1. Explicit timeZone parameter (highest priority)  
            // 2. Calendar's default timezone (fallback)
            // Note: convertToRFC3339 will still respect timezone in datetime string as ultimate override
            let timeMin = options.timeMin;
            let timeMax = options.timeMax;
            
            if (timeMin || timeMax) {
                const timezone = options.timeZone || await this.getCalendarTimezone(client, calendarId);
                timeMin = timeMin ? convertToRFC3339(timeMin, timezone) : undefined;
                timeMax = timeMax ? convertToRFC3339(timeMax, timezone) : undefined;
            }
            
            const fieldMask = buildListFieldMask(options.fields);
            
            const response = await calendar.events.list({
                calendarId,
                timeMin,
                timeMax,
                singleEvents: true,
                orderBy: 'startTime',
                ...(fieldMask && { fields: fieldMask }),
                ...(options.privateExtendedProperty && { privateExtendedProperty: options.privateExtendedProperty as any }),
                ...(options.sharedExtendedProperty && { sharedExtendedProperty: options.sharedExtendedProperty as any })
            });
            
            // Add calendarId to events for consistent interface
            return (response.data.items || []).map(event => ({
                ...event,
                calendarId
            }));
        } catch (error) {
            throw this.handleGoogleApiError(error);
        }
    }

    private async fetchMultipleCalendarEvents(
        client: OAuth2Client,
        calendarIds: string[],
        options: { timeMin?: string; timeMax?: string; timeZone?: string; fields?: string[]; privateExtendedProperty?: string[]; sharedExtendedProperty?: string[] }
    ): Promise<ExtendedEvent[]> {
        const batchHandler = new BatchRequestHandler(client);
        
        const requests = await Promise.all(calendarIds.map(async (calendarId) => ({
            method: "GET" as const,
            path: await this.buildEventsPath(client, calendarId, options)
        })));
        
        const responses = await batchHandler.executeBatch(requests);
        
        const { events, errors } = this.processBatchResponses(responses, calendarIds);
        
        if (errors.length > 0) {
            process.stderr.write(`Some calendars had errors: ${errors.map(e => `${e.calendarId}: ${e.error}`).join(', ')}\n`);
        }
        
        return this.sortEventsByStartTime(events);
    }

    private async buildEventsPath(client: OAuth2Client, calendarId: string, options: { timeMin?: string; timeMax?: string; timeZone?: string; fields?: string[]; privateExtendedProperty?: string[]; sharedExtendedProperty?: string[] }): Promise<string> {
        // Determine timezone with correct precedence:
        // 1. Explicit timeZone parameter (highest priority)
        // 2. Calendar's default timezone (fallback)
        // Note: convertToRFC3339 will still respect timezone in datetime string as ultimate override
        let timeMin = options.timeMin;
        let timeMax = options.timeMax;
        
        if (timeMin || timeMax) {
            const timezone = options.timeZone || await this.getCalendarTimezone(client, calendarId);
            timeMin = timeMin ? convertToRFC3339(timeMin, timezone) : undefined;
            timeMax = timeMax ? convertToRFC3339(timeMax, timezone) : undefined;
        }
        
        const fieldMask = buildListFieldMask(options.fields);
        
        const params = new URLSearchParams({
            singleEvents: "true",
            orderBy: "startTime",
        });
        if (timeMin) params.set('timeMin', timeMin);
        if (timeMax) params.set('timeMax', timeMax);
        if (fieldMask) params.set('fields', fieldMask);
        if (options.privateExtendedProperty) {
            for (const kv of options.privateExtendedProperty) params.append('privateExtendedProperty', kv);
        }
        if (options.sharedExtendedProperty) {
            for (const kv of options.sharedExtendedProperty) params.append('sharedExtendedProperty', kv);
        }
        
        return `/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?${params.toString()}`;
    }

    private processBatchResponses(
        responses: any[], 
        calendarIds: string[]
    ): { events: ExtendedEvent[]; errors: Array<{ calendarId: string; error: string }> } {
        const events: ExtendedEvent[] = [];
        const errors: Array<{ calendarId: string; error: string }> = [];
        
        responses.forEach((response, index) => {
            const calendarId = calendarIds[index];
            
            if (response.statusCode === 200 && response.body?.items) {
                const calendarEvents: ExtendedEvent[] = response.body.items.map((event: any) => ({
                    ...event,
                    calendarId
                }));
                events.push(...calendarEvents);
            } else {
                const errorMessage = response.body?.error?.message || 
                                   response.body?.message || 
                                   `HTTP ${response.statusCode}`;
                errors.push({ calendarId, error: errorMessage });
            }
        });
        
        return { events, errors };
    }

    private sortEventsByStartTime(events: ExtendedEvent[]): ExtendedEvent[] {
        return events.sort((a, b) => {
            const aStart = a.start?.dateTime || a.start?.date || "";
            const bStart = b.start?.dateTime || b.start?.date || "";
            return aStart.localeCompare(bStart);
        });
    }

    private groupEventsByCalendar(events: ExtendedEvent[]): Record<string, ExtendedEvent[]> {
        return events.reduce((acc, event) => {
            const calId = event.calendarId;
            if (!acc[calId]) acc[calId] = [];
            acc[calId].push(event);
            return acc;
        }, {} as Record<string, ExtendedEvent[]>);
    }

}

```

--------------------------------------------------------------------------------
/scripts/account-manager.js:
--------------------------------------------------------------------------------

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

/**
 * Account Manager Script
 * 
 * This script helps manage OAuth tokens for multiple Google accounts:
 * - Normal account: For regular operations
 * - Test account: For integration testing
 * 
 * Usage:
 *   node scripts/account-manager.js list                    # List available accounts
 *   node scripts/account-manager.js auth normal            # Authenticate normal account
 *   node scripts/account-manager.js auth test              # Authenticate test account
 *   node scripts/account-manager.js status                 # Show current account status
 *   node scripts/account-manager.js clear normal           # Clear normal account tokens
 *   node scripts/account-manager.js clear test             # Clear test account tokens
 *   node scripts/account-manager.js test                   # Run tests with test account
 */

import { spawn } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs/promises';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.join(__dirname, '..');

const COLORS = {
  reset: '\x1b[0m',
  bright: '\x1b[1m',
  red: '\x1b[31m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  cyan: '\x1b[36m'
};

function colorize(color, text) {
  return `${COLORS[color]}${text}${COLORS.reset}`;
}

function log(message, color = 'reset') {
  console.log(colorize(color, message));
}

function error(message) {
  console.error(colorize('red', `❌ ${message}`));
}

function success(message) {
  console.log(colorize('green', `✅ ${message}`));
}

function info(message) {
  console.log(colorize('blue', `ℹ️  ${message}`));
}

function warning(message) {
  console.log(colorize('yellow', `⚠️  ${message}`));
}

async function runCommand(command, args, env = {}) {
  return new Promise((resolve, reject) => {
    const fullEnv = { ...process.env, ...env };
    const proc = spawn(command, args, {
      stdio: 'inherit',
      env: fullEnv,
      cwd: projectRoot
    });

    proc.on('close', (code) => {
      if (code === 0) {
        resolve();
      } else {
        reject(new Error(`Command failed with exit code ${code}`));
      }
    });

    proc.on('error', reject);
  });
}

// Import shared path utilities
import { getSecureTokenPath } from '../src/auth/paths.js';

async function loadTokens() {
  const tokenPath = getSecureTokenPath();
  try {
    const content = await fs.readFile(tokenPath, 'utf-8');
    return JSON.parse(content);
  } catch (error) {
    if (error.code === 'ENOENT') {
      return {};
    }
    throw error;
  }
}

async function listAccounts() {
  log('\n' + colorize('bright', '📋 Available Accounts:'));
  
  try {
    const tokens = await loadTokens();
    
    // Check if this is the old single-account format
    if (tokens.access_token || tokens.refresh_token) {
      log('  ' + colorize('yellow', '⚠️  Old token format detected. Will be migrated on next auth.'));
      
      const hasAccessToken = !!tokens.access_token;
      const hasRefreshToken = !!tokens.refresh_token;
      const isExpired = tokens.expiry_date ? Date.now() >= tokens.expiry_date : true;
      
      const status = hasAccessToken && hasRefreshToken && !isExpired ? 
        colorize('green', '✓ Active') : 
        hasRefreshToken ? 
          colorize('yellow', '⟳ Needs Refresh') : 
          colorize('red', '✗ Invalid');
      
      log(`  ${colorize('cyan', 'normal'.padEnd(10))} ${status} (legacy format)`);
      return;
    }
    
    // New multi-account format
    const accounts = Object.keys(tokens);
    
    if (accounts.length === 0) {
      warning('No accounts found. Use "auth" command to authenticate.');
      return;
    }
    
    for (const account of accounts) {
      const tokenInfo = tokens[account];
      const hasAccessToken = !!tokenInfo.access_token;
      const hasRefreshToken = !!tokenInfo.refresh_token;
      const isExpired = tokenInfo.expiry_date ? Date.now() >= tokenInfo.expiry_date : true;
      
      const status = hasAccessToken && hasRefreshToken && !isExpired ? 
        colorize('green', '✓ Active') : 
        hasRefreshToken ? 
          colorize('yellow', '⟳ Needs Refresh') : 
          colorize('red', '✗ Invalid');
      
      log(`  ${colorize('cyan', account.padEnd(10))} ${status}`);
    }
  } catch (error) {
    error(`Failed to load token information: ${error.message}`);
  }
}

async function authenticateAccount(accountMode) {
  if (!['normal', 'test'].includes(accountMode)) {
    error('Account mode must be "normal" or "test"');
    process.exit(1);
  }
  
  log(`\n🔐 Authenticating ${colorize('cyan', accountMode)} account...`);
  
  try {
    await runCommand('npm', ['run', 'auth'], {
      GOOGLE_ACCOUNT_MODE: accountMode
    });
    success(`Successfully authenticated ${accountMode} account!`);
  } catch (error) {
    error(`Failed to authenticate ${accountMode} account: ${error.message}`);
    process.exit(1);
  }
}

async function showStatus() {
  log('\n' + colorize('bright', '📊 Account Status:'));
  
  const currentMode = process.env.GOOGLE_ACCOUNT_MODE || 'normal';
  log(`  Current Mode: ${colorize('cyan', currentMode)}`);
  
  await listAccounts();
  
  // Show environment variables relevant to testing
  log('\n' + colorize('bright', '🧪 Test Configuration:'));
  const testVars = [
    'TEST_CALENDAR_ID',
    'INVITEE_1', 
    'INVITEE_2',
    'CLAUDE_API_KEY'
  ];
  
  for (const varName of testVars) {
    const value = process.env[varName];
    if (value) {
      const displayValue = varName === 'CLAUDE_API_KEY' ? 
        value.substring(0, 8) + '...' : value;
      log(`  ${varName.padEnd(20)}: ${colorize('green', displayValue)}`);
    } else {
      log(`  ${varName.padEnd(20)}: ${colorize('red', 'Not set')}`);
    }
  }
}

async function clearAccount(accountMode) {
  if (!['normal', 'test'].includes(accountMode)) {
    error('Account mode must be "normal" or "test"');
    process.exit(1);
  }
  
  log(`\n🗑️  Clearing ${colorize('cyan', accountMode)} account tokens...`);
  
  try {
    const tokens = await loadTokens();
    
    if (!tokens[accountMode]) {
      warning(`No tokens found for ${accountMode} account`);
      return;
    }
    
    delete tokens[accountMode];
    
    const tokenPath = getSecureTokenPath();
    
    if (Object.keys(tokens).length === 0) {
      await fs.unlink(tokenPath);
      success('All tokens cleared, file deleted');
    } else {
      await fs.writeFile(tokenPath, JSON.stringify(tokens, null, 2), { mode: 0o600 });
      success(`Cleared tokens for ${accountMode} account`);
    }
  } catch (error) {
    error(`Failed to clear ${accountMode} account: ${error.message}`);
    process.exit(1);
  }
}

async function runTests() {
  log('\n🧪 Running integration tests with test account...');
  
  try {
    await runCommand('npm', ['test'], {
      GOOGLE_ACCOUNT_MODE: 'test'
    });
    success('Tests completed successfully!');
  } catch (error) {
    error(`Tests failed: ${error.message}`);
    process.exit(1);
  }
}

function showUsage() {
  log('\n' + colorize('bright', 'Google Calendar Account Manager'));
  log('\nManage OAuth tokens for multiple Google accounts (normal & test)');
  log('\n' + colorize('bright', 'Usage:'));
  log('  node scripts/account-manager.js <command> [args]');
  log('\n' + colorize('bright', 'Commands:'));
  log('  list                    List available accounts and their status');
  log('  auth <normal|test>      Authenticate the specified account');
  log('  status                  Show current account status and configuration');
  log('  clear <normal|test>     Clear tokens for the specified account');
  log('  test                    Run integration tests with test account');
  log('  help                    Show this help message');
  log('\n' + colorize('bright', 'Examples:'));
  log('  node scripts/account-manager.js auth test     # Authenticate test account');
  log('  node scripts/account-manager.js test          # Run tests with test account');
  log('  node scripts/account-manager.js status        # Check account status');
  log('\n' + colorize('bright', 'Environment Variables:'));
  log('  GOOGLE_ACCOUNT_MODE     Set to "test" or "normal" (default: normal)');
  log('  TEST_CALENDAR_ID        Calendar ID to use for testing');
  log('  INVITEE_1, INVITEE_2    Email addresses for testing invitations');
  log('  CLAUDE_API_KEY          API key for Claude integration tests');
}

async function main() {
  const command = process.argv[2];
  const arg = process.argv[3];
  
  switch (command) {
    case 'list':
      await listAccounts();
      break;
    case 'auth':
      if (!arg) {
        error('Please specify account mode: normal or test');
        process.exit(1);
      }
      await authenticateAccount(arg);
      break;
    case 'status':
      await showStatus();
      break;
    case 'clear':
      if (!arg) {
        error('Please specify account mode: normal or test');
        process.exit(1);
      }
      await clearAccount(arg);
      break;
    case 'test':
      await runTests();
      break;
    case 'help':
    case '--help':
    case '-h':
      showUsage();
      break;
    default:
      if (command) {
        error(`Unknown command: ${command}`);
      }
      showUsage();
      process.exit(1);
  }
}

// Handle uncaught errors
process.on('unhandledRejection', (reason, promise) => {
  error(`Unhandled rejection at: ${promise}, reason: ${reason}`);
  process.exit(1);
});

process.on('uncaughtException', (error) => {
  error(`Uncaught exception: ${error.message}`);
  process.exit(1);
});

main().catch((error) => {
  error(`Script failed: ${error.message}`);
  process.exit(1);
}); 
```

--------------------------------------------------------------------------------
/src/types/structured-responses.ts:
--------------------------------------------------------------------------------

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

/**
 * Represents a date/time value in Google Calendar API format
 */
export interface DateTime {
  dateTime?: string;
  date?: string;
  timeZone?: string;
}

/**
 * Represents an event attendee with their response status and details
 */
export interface Attendee {
  email: string;
  displayName?: string;
  responseStatus?: 'needsAction' | 'declined' | 'tentative' | 'accepted';
  optional?: boolean;
  organizer?: boolean;
  self?: boolean;
  resource?: boolean;
  comment?: string;
  additionalGuests?: number;
}

/**
 * Conference/meeting information for an event (e.g., Google Meet, Zoom)
 */
export interface ConferenceData {
  conferenceId?: string;
  conferenceSolution?: {
    key?: { type?: string };
    name?: string;
    iconUri?: string;
  };
  entryPoints?: Array<{
    entryPointType?: string;
    uri?: string;
    label?: string;
    pin?: string;
    accessCode?: string;
    meetingCode?: string;
    passcode?: string;
    password?: string;
  }>;
  createRequest?: {
    requestId?: string;
    conferenceSolutionKey?: { type?: string };
    status?: { statusCode?: string };
  };
  parameters?: {
    addOnParameters?: {
      parameters?: Record<string, string>;
    };
  };
}

/**
 * Custom key-value pairs for storing additional event metadata
 */
export interface ExtendedProperties {
  private?: Record<string, string>;
  shared?: Record<string, string>;
}

/**
 * Event reminder configuration
 */
export interface Reminder {
  method: 'email' | 'popup';
  minutes: number;
}

/**
 * Complete structured representation of a Google Calendar event
 */
export interface StructuredEvent {
  id: string;
  summary?: string;
  description?: string;
  location?: string;
  start: DateTime;
  end: DateTime;
  status?: string;
  htmlLink?: string;
  created?: string;
  updated?: string;
  colorId?: string;
  creator?: {
    email?: string;
    displayName?: string;
    self?: boolean;
  };
  organizer?: {
    email?: string;
    displayName?: string;
    self?: boolean;
  };
  attendees?: Attendee[];
  recurrence?: string[];
  recurringEventId?: string;
  originalStartTime?: DateTime;
  transparency?: 'opaque' | 'transparent';
  visibility?: 'default' | 'public' | 'private' | 'confidential';
  iCalUID?: string;
  sequence?: number;
  reminders?: {
    useDefault?: boolean;
    overrides?: Reminder[];
  };
  source?: {
    url?: string;
    title?: string;
  };
  attachments?: Array<{
    fileUrl?: string;
    title?: string;
    mimeType?: string;
    iconLink?: string;
    fileId?: string;
  }>;
  eventType?: 'default' | 'outOfOffice' | 'focusTime' | 'workingLocation';
  conferenceData?: ConferenceData;
  extendedProperties?: ExtendedProperties;
  hangoutLink?: string;
  anyoneCanAddSelf?: boolean;
  guestsCanInviteOthers?: boolean;
  guestsCanModify?: boolean;
  guestsCanSeeOtherGuests?: boolean;
  privateCopy?: boolean;
  locked?: boolean;
  calendarId?: string;
}

/**
 * Information about a scheduling conflict with another event
 */
export interface ConflictInfo {
  event: {
    id: string;
    title: string;
    start: string;
    end: string;
    url?: string;
    similarity?: number;
  };
  calendar: string;
  overlap?: {
    duration: string;
    percentage: string;
  };
  suggestion?: string;
}

/**
 * Information about a potential duplicate event
 */
export interface DuplicateInfo {
  event: {
    id: string;
    title: string;
    start: string;
    end: string;
    url?: string;
    similarity: number;
  };
  calendarId: string;
  suggestion: string;
}

/**
 * Response format for listing calendar events
 */
export interface ListEventsResponse {
  events: StructuredEvent[];
  totalCount: number;
  calendars?: string[];
}

/**
 * Response format for searching calendar events
 */
export interface SearchEventsResponse {
  events: StructuredEvent[];
  totalCount: number;
  query: string;
  calendarId: string;
  timeRange?: {
    start: string;
    end: string;
  };
}

/**
 * Response format for getting a single event by ID
 */
export interface GetEventResponse {
  event: StructuredEvent;
}

/**
 * Response format for creating a new event
 */
export interface CreateEventResponse {
  event: StructuredEvent;
  conflicts?: ConflictInfo[];
  duplicates?: DuplicateInfo[];
  warnings?: string[];
}

/**
 * Response format for updating an existing event
 */
export interface UpdateEventResponse {
  event: StructuredEvent;
  conflicts?: ConflictInfo[];
  warnings?: string[];
}

/**
 * Response format for deleting an event
 */
export interface DeleteEventResponse {
  success: boolean;
  eventId: string;
  calendarId: string;
  message?: string;
}

/**
 * Detailed information about a calendar
 */
export interface CalendarInfo {
  id: string;
  summary?: string;
  description?: string;
  location?: string;
  timeZone?: string;
  summaryOverride?: string;
  colorId?: string;
  backgroundColor?: string;
  foregroundColor?: string;
  hidden?: boolean;
  selected?: boolean;
  accessRole?: string;
  defaultReminders?: Reminder[];
  notificationSettings?: {
    notifications?: Array<{
      type?: string;
      method?: string;
    }>;
  };
  primary?: boolean;
  deleted?: boolean;
  conferenceProperties?: {
    allowedConferenceSolutionTypes?: string[];
  };
}

/**
 * Response format for listing available calendars
 */
export interface ListCalendarsResponse {
  calendars: CalendarInfo[];
  totalCount: number;
}

/**
 * Color scheme definition with background and foreground colors
 */
export interface ColorDefinition {
  background: string;
  foreground: string;
}

/**
 * Response format for available calendar and event colors
 */
export interface ListColorsResponse {
  event: Record<string, ColorDefinition>;
  calendar: Record<string, ColorDefinition>;
}

/**
 * Represents a busy time period in free/busy queries
 */
export interface BusySlot {
  start: string;
  end: string;
}

/**
 * Response format for free/busy time queries
 */
export interface FreeBusyResponse {
  timeMin: string;
  timeMax: string;
  calendars: Record<string, {
    busy: BusySlot[];
    errors?: Array<{
      domain?: string;
      reason?: string;
    }>;
  }>;
}

/**
 * Response format for getting the current time in a specific timezone
 */
export interface GetCurrentTimeResponse {
  currentTime: string;
  timezone: string;
  offset: string;
  isDST?: boolean;
}

/**
 * Converts a Google Calendar API event to our structured format
 * @param event - The Google Calendar API event object
 * @param calendarId - Optional calendar ID to include in the response
 * @returns Structured event representation
 */
export function convertGoogleEventToStructured(
  event: calendar_v3.Schema$Event,
  calendarId?: string
): StructuredEvent {
  return {
    id: event.id || '',
    summary: event.summary ?? undefined,
    description: event.description ?? undefined,
    location: event.location ?? undefined,
    start: {
      dateTime: event.start?.dateTime ?? undefined,
      date: event.start?.date ?? undefined,
      timeZone: event.start?.timeZone ?? undefined,
    },
    end: {
      dateTime: event.end?.dateTime ?? undefined,
      date: event.end?.date ?? undefined,
      timeZone: event.end?.timeZone ?? undefined,
    },
    status: event.status ?? undefined,
    htmlLink: event.htmlLink ?? undefined,
    created: event.created ?? undefined,
    updated: event.updated ?? undefined,
    colorId: event.colorId ?? undefined,
    creator: event.creator ? {
      email: event.creator.email ?? '',
      displayName: event.creator.displayName ?? undefined,
      self: event.creator.self ?? undefined,
    } : undefined,
    organizer: event.organizer ? {
      email: event.organizer.email ?? '',
      displayName: event.organizer.displayName ?? undefined,
      self: event.organizer.self ?? undefined,
    } : undefined,
    attendees: event.attendees?.map(a => ({
      email: a.email || '',
      displayName: a.displayName ?? undefined,
      responseStatus: a.responseStatus as any,
      optional: a.optional ?? undefined,
      organizer: a.organizer ?? undefined,
      self: a.self ?? undefined,
      resource: a.resource ?? undefined,
      comment: a.comment ?? undefined,
      additionalGuests: a.additionalGuests ?? undefined,
    })),
    recurrence: event.recurrence ?? undefined,
    recurringEventId: event.recurringEventId ?? undefined,
    originalStartTime: event.originalStartTime ? {
      dateTime: event.originalStartTime.dateTime ?? undefined,
      date: event.originalStartTime.date ?? undefined,
      timeZone: event.originalStartTime.timeZone ?? undefined,
    } : undefined,
    transparency: event.transparency as any,
    visibility: event.visibility as any,
    iCalUID: event.iCalUID ?? undefined,
    sequence: event.sequence ?? undefined,
    reminders: event.reminders ? {
      useDefault: event.reminders.useDefault ?? undefined,
      overrides: event.reminders.overrides?.map(r => ({
        method: (r.method as any) || 'popup',
        minutes: r.minutes || 0,
      })),
    } : undefined,
    source: event.source ? {
      url: event.source.url ?? undefined,
      title: event.source.title ?? undefined,
    } : undefined,
    attachments: event.attachments?.map(a => ({
      fileUrl: a.fileUrl ?? undefined,
      title: a.title ?? undefined,
      mimeType: a.mimeType ?? undefined,
      iconLink: a.iconLink ?? undefined,
      fileId: a.fileId ?? undefined,
    })),
    eventType: event.eventType as any,
    conferenceData: event.conferenceData as ConferenceData,
    extendedProperties: event.extendedProperties as ExtendedProperties,
    hangoutLink: event.hangoutLink ?? undefined,
    anyoneCanAddSelf: event.anyoneCanAddSelf ?? undefined,
    guestsCanInviteOthers: event.guestsCanInviteOthers ?? undefined,
    guestsCanModify: event.guestsCanModify ?? undefined,
    guestsCanSeeOtherGuests: event.guestsCanSeeOtherGuests ?? undefined,
    privateCopy: event.privateCopy ?? undefined,
    locked: event.locked ?? undefined,
    calendarId: calendarId,
  };
}
```

--------------------------------------------------------------------------------
/src/tests/unit/schemas/enhanced-properties.test.ts:
--------------------------------------------------------------------------------

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

describe('Enhanced Create-Event Properties', () => {
  const createEventSchema = ToolSchemas['create-event'];
  
  const baseEvent = {
    calendarId: 'primary',
    summary: 'Test Event',
    start: '2025-01-20T10:00:00',
    end: '2025-01-20T11:00:00'
  };

  describe('Guest Management Properties', () => {
    it('should accept transparency values', () => {
      expect(() => createEventSchema.parse({
        ...baseEvent,
        transparency: 'opaque'
      })).not.toThrow();
      
      expect(() => createEventSchema.parse({
        ...baseEvent,
        transparency: 'transparent'
      })).not.toThrow();
    });

    it('should reject invalid transparency values', () => {
      expect(() => createEventSchema.parse({
        ...baseEvent,
        transparency: 'invalid'
      })).toThrow();
    });

    it('should accept visibility values', () => {
      const validVisibilities = ['default', 'public', 'private', 'confidential'];
      validVisibilities.forEach(visibility => {
        expect(() => createEventSchema.parse({
          ...baseEvent,
          visibility
        })).not.toThrow();
      });
    });

    it('should accept guest permission booleans', () => {
      const event = {
        ...baseEvent,
        guestsCanInviteOthers: false,
        guestsCanModify: true,
        guestsCanSeeOtherGuests: false,
        anyoneCanAddSelf: true
      };
      expect(() => createEventSchema.parse(event)).not.toThrow();
    });

    it('should accept sendUpdates values', () => {
      const validSendUpdates = ['all', 'externalOnly', 'none'];
      validSendUpdates.forEach(sendUpdates => {
        expect(() => createEventSchema.parse({
          ...baseEvent,
          sendUpdates
        })).not.toThrow();
      });
    });
  });

  describe('Conference Data', () => {
    it('should accept valid conference data', () => {
      const event = {
        ...baseEvent,
        conferenceData: {
          createRequest: {
            requestId: 'unique-123',
            conferenceSolutionKey: {
              type: 'hangoutsMeet'
            }
          }
        }
      };
      expect(() => createEventSchema.parse(event)).not.toThrow();
    });

    it('should accept all conference solution types', () => {
      const types = ['hangoutsMeet', 'eventHangout', 'eventNamedHangout', 'addOn'];
      types.forEach(type => {
        const event = {
          ...baseEvent,
          conferenceData: {
            createRequest: {
              requestId: `req-${type}`,
              conferenceSolutionKey: { type }
            }
          }
        };
        expect(() => createEventSchema.parse(event)).not.toThrow();
      });
    });

    it('should reject conference data without required fields', () => {
      expect(() => createEventSchema.parse({
        ...baseEvent,
        conferenceData: {
          createRequest: {
            requestId: 'test'
            // Missing conferenceSolutionKey
          }
        }
      })).toThrow();
    });
  });

  describe('Extended Properties', () => {
    it('should accept extended properties', () => {
      const event = {
        ...baseEvent,
        extendedProperties: {
          private: {
            key1: 'value1',
            key2: 'value2'
          },
          shared: {
            sharedKey: 'sharedValue'
          }
        }
      };
      expect(() => createEventSchema.parse(event)).not.toThrow();
    });

    it('should accept only private properties', () => {
      const event = {
        ...baseEvent,
        extendedProperties: {
          private: { app: 'myapp' }
        }
      };
      expect(() => createEventSchema.parse(event)).not.toThrow();
    });

    it('should accept only shared properties', () => {
      const event = {
        ...baseEvent,
        extendedProperties: {
          shared: { category: 'meeting' }
        }
      };
      expect(() => createEventSchema.parse(event)).not.toThrow();
    });

    it('should accept empty extended properties object', () => {
      const event = {
        ...baseEvent,
        extendedProperties: {}
      };
      expect(() => createEventSchema.parse(event)).not.toThrow();
    });
  });

  describe('Attachments', () => {
    it('should accept attachments array', () => {
      const event = {
        ...baseEvent,
        attachments: [
          {
            fileUrl: 'https://example.com/file.pdf',
            title: 'Document',
            mimeType: 'application/pdf',
            iconLink: 'https://example.com/icon.png',
            fileId: 'file123'
          }
        ]
      };
      expect(() => createEventSchema.parse(event)).not.toThrow();
    });

    it('should accept minimal attachment (only fileUrl)', () => {
      const event = {
        ...baseEvent,
        attachments: [
          { fileUrl: 'https://example.com/file.pdf' }
        ]
      };
      expect(() => createEventSchema.parse(event)).not.toThrow();
    });

    it('should accept multiple attachments', () => {
      const event = {
        ...baseEvent,
        attachments: [
          { fileUrl: 'https://example.com/file1.pdf' },
          { fileUrl: 'https://example.com/file2.doc', title: 'Doc' },
          { fileUrl: 'https://example.com/file3.xls', mimeType: 'application/excel' }
        ]
      };
      expect(() => createEventSchema.parse(event)).not.toThrow();
    });

    it('should reject attachments without fileUrl', () => {
      expect(() => createEventSchema.parse({
        ...baseEvent,
        attachments: [
          { title: 'Document' } // Missing fileUrl
        ]
      })).toThrow();
    });
  });

  describe('Enhanced Attendees', () => {
    it('should accept attendees with all optional fields', () => {
      const event = {
        ...baseEvent,
        attendees: [
          {
            email: '[email protected]',
            displayName: 'Test User',
            optional: true,
            responseStatus: 'accepted',
            comment: 'Looking forward to it',
            additionalGuests: 2
          }
        ]
      };
      expect(() => createEventSchema.parse(event)).not.toThrow();
    });

    it('should accept all response status values', () => {
      const statuses = ['needsAction', 'declined', 'tentative', 'accepted'];
      statuses.forEach(responseStatus => {
        const event = {
          ...baseEvent,
          attendees: [
            { email: '[email protected]', responseStatus }
          ]
        };
        expect(() => createEventSchema.parse(event)).not.toThrow();
      });
    });

    it('should accept attendees with only email', () => {
      const event = {
        ...baseEvent,
        attendees: [
          { email: '[email protected]' }
        ]
      };
      expect(() => createEventSchema.parse(event)).not.toThrow();
    });

    it('should reject attendees without email', () => {
      expect(() => createEventSchema.parse({
        ...baseEvent,
        attendees: [
          { displayName: 'No Email User' }
        ]
      })).toThrow();
    });

    it('should reject negative additional guests', () => {
      expect(() => createEventSchema.parse({
        ...baseEvent,
        attendees: [
          { email: '[email protected]', additionalGuests: -1 }
        ]
      })).toThrow();
    });
  });

  describe('Source Property', () => {
    it('should accept source with url and title', () => {
      const event = {
        ...baseEvent,
        source: {
          url: 'https://example.com/event/123',
          title: 'External Event System'
        }
      };
      expect(() => createEventSchema.parse(event)).not.toThrow();
    });

    it('should reject source without url', () => {
      expect(() => createEventSchema.parse({
        ...baseEvent,
        source: { title: 'No URL' }
      })).toThrow();
    });

    it('should reject source without title', () => {
      expect(() => createEventSchema.parse({
        ...baseEvent,
        source: { url: 'https://example.com' }
      })).toThrow();
    });
  });

  describe('Combined Properties', () => {
    it('should accept event with all enhanced properties', () => {
      const complexEvent = {
        ...baseEvent,
        eventId: 'custom-id-123',
        description: 'Complex event with all features',
        location: 'Conference Room',
        transparency: 'opaque',
        visibility: 'public',
        guestsCanInviteOthers: true,
        guestsCanModify: false,
        guestsCanSeeOtherGuests: true,
        anyoneCanAddSelf: false,
        sendUpdates: 'all',
        conferenceData: {
          createRequest: {
            requestId: 'conf-123',
            conferenceSolutionKey: { type: 'hangoutsMeet' }
          }
        },
        extendedProperties: {
          private: { appId: '123' },
          shared: { category: 'meeting' }
        },
        attachments: [
          { fileUrl: 'https://example.com/agenda.pdf', title: 'Agenda' }
        ],
        attendees: [
          {
            email: '[email protected]',
            displayName: 'Alice',
            optional: false,
            responseStatus: 'accepted'
          },
          {
            email: '[email protected]',
            displayName: 'Bob',
            optional: true,
            responseStatus: 'tentative',
            additionalGuests: 1
          }
        ],
        source: {
          url: 'https://example.com/source',
          title: 'Source System'
        },
        colorId: '5',
        reminders: {
          useDefault: false,
          overrides: [{ method: 'popup', minutes: 15 }]
        }
      };
      
      expect(() => createEventSchema.parse(complexEvent)).not.toThrow();
    });

    it('should maintain backward compatibility with minimal event', () => {
      // Only required fields
      const minimalEvent = {
        calendarId: 'primary',
        summary: 'Simple Event',
        start: '2025-01-20T10:00:00',
        end: '2025-01-20T11:00:00'
      };
      
      expect(() => createEventSchema.parse(minimalEvent)).not.toThrow();
    });
  });
});
```

--------------------------------------------------------------------------------
/src/tests/integration/claude-mcp-integration.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import Anthropic from '@anthropic-ai/sdk';
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

/**
 * Minimal Claude + MCP Integration Tests
 * 
 * PURPOSE: Test ONLY what's unique to LLM integration:
 * 1. Can Claude understand user intent and select appropriate tools?
 * 2. Can Claude handle multi-step reasoning?
 * 3. Can Claude handle ambiguous requests appropriately?
 * 
 * NOT TESTED HERE (covered in direct-integration.test.ts):
 * - Tool functionality
 * - Conflict detection
 * - Calendar operations
 * - Error handling
 * - Performance
 */

interface LLMResponse {
  content: string;
  toolCalls: Array<{ name: string; arguments: Record<string, any> }>;
  executedResults: Array<{ 
    toolCall: { name: string; arguments: Record<string, any> };
    result: any;
    success: boolean;
  }>;
}

class ClaudeMCPClient {
  private anthropic: Anthropic;
  private mcpClient: Client;
  
  constructor(apiKey: string, mcpClient: Client) {
    this.anthropic = new Anthropic({ apiKey });
    this.mcpClient = mcpClient;
  }
  
  async sendMessage(prompt: string): Promise<LLMResponse> {
    // Get available tools from MCP server
    const availableTools = await this.mcpClient.listTools();
    const model = process.env.ANTHROPIC_MODEL ?? 'claude-sonnet-4-5-20250929';
    
    // Convert MCP tools to Claude format
    const claudeTools = availableTools.tools.map(tool => ({
      name: tool.name,
      description: tool.description,
      input_schema: tool.inputSchema
    }));
    
    // Send to Claude
    const message = await this.anthropic.messages.create({
      model,
      max_tokens: 2500,
      tools: claudeTools,
      messages: [{
        role: 'user' as const,
        content: prompt
      }]
    });
    
    // Extract tool calls
    const toolCalls: Array<{ name: string; arguments: Record<string, any> }> = [];
    let textContent = '';
    
    message.content.forEach(content => {
      if (content.type === 'text') {
        textContent += content.text;
      } else if (content.type === 'tool_use') {
        toolCalls.push({
          name: content.name,
          arguments: content.input as Record<string, any>
        });
      }
    });
    
    // Execute tool calls
    const executedResults = [];
    for (const toolCall of toolCalls) {
      try {
        const result = await this.mcpClient.callTool({
          name: toolCall.name,
          arguments: toolCall.arguments
        });
        
        executedResults.push({
          toolCall,
          result,
          success: true
        });
      } catch (error) {
        executedResults.push({
          toolCall,
          result: { error: String(error) },
          success: false
        });
      }
    }
    
    return {
      content: textContent,
      toolCalls,
      executedResults
    };
  }
}

describe('Claude + MCP Essential Tests', () => {
  let mcpClient: Client;
  let claudeClient: ClaudeMCPClient;
  
  beforeAll(async () => {
    // Start MCP server
    const cleanEnv = Object.fromEntries(
      Object.entries(process.env).filter(([_, value]) => value !== undefined)
    ) as Record<string, string>;
    cleanEnv.NODE_ENV = 'test';
    
    // Create MCP client
    mcpClient = new Client({
      name: "minimal-test-client",
      version: "1.0.0"
    }, {
      capabilities: { tools: {} }
    });
    
    // Connect to server
    const transport = new StdioClientTransport({
      command: 'node',
      args: ['build/index.js'],
      env: cleanEnv
    });
    
    await mcpClient.connect(transport);
    
    // Initialize Claude client
    const apiKey = process.env.CLAUDE_API_KEY;
    if (!apiKey) {
      throw new Error('CLAUDE_API_KEY not set');
    }
    
    claudeClient = new ClaudeMCPClient(apiKey, mcpClient);
    
    // Verify connection
    const tools = await mcpClient.listTools();
    console.log(`Connected to MCP with ${tools.tools.length} tools available`);
  }, 30000);
  
  afterAll(async () => {
    if (mcpClient) await mcpClient.close();
  }, 10000);

  describe('Core LLM Capabilities', () => {
    it('should select appropriate tools for user intent', async () => {
      const testCases = [
        {
          intent: 'create',
          prompt: 'Schedule a meeting tomorrow at 3 PM',
          expectedTools: ['create-event', 'get-current-time']
        },
        {
          intent: 'search',
          prompt: 'Find my meetings with Sarah',
          expectedTools: ['search-events', 'list-events', 'get-current-time']
        },
        {
          intent: 'availability',
          prompt: 'Am I free tomorrow afternoon?',
          expectedTools: ['get-freebusy', 'list-events', 'get-current-time']
        }
      ];
      
      for (const test of testCases) {
        const response = await claudeClient.sendMessage(test.prompt);
        
        // Check if Claude used one of the expected tools
        const usedExpectedTool = response.toolCalls.some(tc =>
          test.expectedTools.includes(tc.name)
        );
        
        // Or at least understood the intent in its response
        const understoodIntent = 
          usedExpectedTool ||
          response.content.toLowerCase().includes(test.intent);
        
        expect(understoodIntent).toBe(true);
      }
    }, 60000);
    
    it('should handle multi-step requests', async () => {
      const response = await claudeClient.sendMessage(
        'What time is it now, and do I have any meetings in the next 2 hours?'
      );
      
      // This requires multiple tool calls or understanding multiple parts
      const handledMultiStep = 
        response.toolCalls.length > 1 || // Multiple tools used
        (response.toolCalls.some(tc => tc.name === 'get-current-time') &&
         response.toolCalls.some(tc => tc.name === 'list-events')) || // Both time and events
        (response.content.includes('time') && response.content.includes('meeting')); // Understood both parts
      
      expect(handledMultiStep).toBe(true);
    }, 30000);
    
    it('should handle ambiguous requests gracefully', async () => {
      const response = await claudeClient.sendMessage(
        'Set up the usual'
      );
      
      // Claude should either:
      // 1. Ask for clarification
      // 2. Make a reasonable attempt with available context
      // 3. Explain what information is needed
      const handledGracefully = 
        response.content.toLowerCase().includes('what') ||
        response.content.toLowerCase().includes('specify') ||
        response.content.toLowerCase().includes('usual') ||
        response.content.toLowerCase().includes('more') ||
        response.toolCalls.length > 0; // Or attempts something
      
      expect(handledGracefully).toBe(true);
    }, 30000);
  });
  
  describe('Tool Selection Accuracy', () => {
    it('should distinguish between list and search operations', async () => {
      // Specific search should use search-events
      const searchResponse = await claudeClient.sendMessage(
        'Find meetings about project alpha'
      );
      
      const usedSearch = 
        searchResponse.toolCalls.some(tc => tc.name === 'search-events') ||
        searchResponse.content.toLowerCase().includes('search');
      
      // General list should use list-events
      const listResponse = await claudeClient.sendMessage(
        'Show me tomorrow\'s schedule'
      );
      
      const usedList = 
        listResponse.toolCalls.some(tc => tc.name === 'list-events') ||
        listResponse.content.toLowerCase().includes('tomorrow');
      
      // At least one should be correct
      expect(usedSearch || usedList).toBe(true);
    }, 30000);
    
    it('should understand when NOT to use tools', async () => {
      const response = await claudeClient.sendMessage(
        'How does Google Calendar handle recurring events?'
      );
      
      // This is a question about calendars, not a calendar operation
      // Claude should either:
      // 1. Not use tools and explain
      // 2. Use minimal tools (like list-calendars) to provide context
      const appropriateResponse = 
        response.toolCalls.length === 0 || // No tools
        response.toolCalls.length === 1 && response.toolCalls[0].name === 'list-calendars' || // Just checking calendars
        response.content.toLowerCase().includes('recurring'); // Explains about recurring events
      
      expect(appropriateResponse).toBe(true);
    }, 30000);
  });
  
  describe('Context Understanding', () => {
    it('should understand relative time expressions', async () => {
      const testPhrases = [
        'tomorrow at 2 PM',
        'next Monday',
        'in 30 minutes'
      ];
      
      for (const phrase of testPhrases) {
        const response = await claudeClient.sendMessage(
          `Schedule a meeting ${phrase}`
        );
        
        // Claude should either get current time or attempt to create an event
        const understoodTime = 
          response.toolCalls.some(tc => 
            tc.name === 'get-current-time' || 
            tc.name === 'create-event'
          ) ||
          response.content.toLowerCase().includes(phrase.split(' ')[0]); // References the time
        
        expect(understoodTime).toBe(true);
      }
    }, 60000);
  });
});

/**
 * What we removed:
 * ✂️ All conflict detection tests (tested in direct integration)
 * ✂️ Duplicate detection tests (tested in direct integration)
 * ✂️ Conference room booking tests (business logic, not LLM)
 * ✂️ Back-to-back meeting tests (calendar logic, not LLM)
 * ✂️ Specific warning message tests (tool behavior, not LLM)
 * ✂️ Performance tests (server performance, not LLM)
 * ✂️ Complex multi-event creation tests (tool functionality)
 * 
 * What remains:
 * ✅ Tool selection for different intents (core LLM capability)
 * ✅ Multi-step request handling (LLM reasoning)
 * ✅ Ambiguous request handling (LLM robustness)
 * ✅ Context understanding (LLM comprehension)
 * ✅ Knowing when NOT to use tools (LLM judgment)
 */
```

--------------------------------------------------------------------------------
/docs/testing.md:
--------------------------------------------------------------------------------

```markdown
# Testing Guide

## Quick Start

```bash
npm test                 # Unit tests (no auth required)
npm run test:integration # Integration tests (requires Google auth)
npm run test:all         # All tests (requires Google auth + LLM API keys)
```

## Test Structure

- `src/tests/unit/` - Unit tests (mocked, no external dependencies)
- `src/tests/integration/` - Integration tests (real Google Calendar API calls)

## Unit Tests

**Requirements:** None - fully self-contained

**Coverage:**
- Request validation and schema compliance
- Error handling and edge cases
- Date/time parsing and timezone conversion logic
- Mock-based handler functionality
- Tool registration and validation

**Run with:**
```bash
npm test
```

## Integration Tests

Integration tests are divided into three categories based on their requirements:

### 1. Direct Google Calendar Integration

**Files:** `direct-integration.test.ts`

**Requirements:**
- Google OAuth credentials file
- Authenticated test account
- Real Google Calendar access

**Setup:**
```bash
# Set environment variables
export GOOGLE_OAUTH_CREDENTIALS="path/to/your/oauth-credentials.json"
export TEST_CALENDAR_ID="your-test-calendar-id"

# Authenticate test account
npm run dev auth:test
```

**What these tests do:**
- ✅ Create, read, update, delete real calendar events
- ✅ Test multi-calendar operations with batch requests
- ✅ Validate timezone handling with actual Google Calendar API
- ✅ Test recurring event patterns and modifications
- ✅ Verify free/busy queries and calendar listings
- ✅ Performance benchmarking with real API latency

**⚠️ Warning:** These tests modify real calendar data in your test calendar.

### 2. LLM Integration Tests

**Files:** `claude-mcp-integration.test.ts`, `openai-mcp-integration.test.ts`

**Requirements:**
- Google OAuth credentials + authenticated test account (from above)
- LLM API keys
- **LLM models that support MCP (Claude) or function calling (OpenAI)**

**Additional setup:**
```bash
# Set LLM API keys
export CLAUDE_API_KEY="your-claude-api-key"
export OPENAI_API_KEY="your-openai-api-key"

# Optional: specify models (must support MCP/function calling)
export ANTHROPIC_MODEL="claude-3-5-haiku-20241022"  # Default
export OPENAI_MODEL="gpt-4o-mini"                   # Default
```

**What these tests do:**
- ✅ Test end-to-end MCP protocol integration with Claude
- ✅ Test end-to-end MCP protocol integration with OpenAI
- ✅ Validate AI assistant can successfully call calendar tools
- ✅ Test complex multi-step AI workflows

**⚠️ Warning:** These tests consume LLM API credits and modify real calendar data.

**Important LLM Compatibility Notes:**
- **Claude**: Only Claude 3.5+ models support MCP. Earlier models will fail.
- **OpenAI**: Only GPT-4+ and select GPT-3.5-turbo models support function calling.
- If you see "tool not found" or "function not supported" errors, verify your model selection.

### 3. Docker Integration Tests

**Files:** `docker-integration.test.ts`

**Requirements:**
- Docker installed and running
- Google OAuth credentials

**What these tests do:**
- ✅ Test containerized deployment
- ✅ Validate HTTP transport mode
- ✅ Test Docker environment configuration

### Running Specific Integration Test Types

```bash
# Run only direct Google Calendar integration tests
npm run test:integration -- direct-integration.test.ts

# Run only LLM integration tests (requires API keys)
npm run test:integration -- claude-mcp-integration.test.ts
npm run test:integration -- openai-mcp-integration.test.ts

# Run all integration tests (requires both Google auth + LLM API keys)
npm run test:integration
```

## Environment Configuration

### Required Environment Variables

| Variable | Required For | Purpose | Example |
|----------|--------------|---------|---------|
| `GOOGLE_OAUTH_CREDENTIALS` | All integration tests | Path to OAuth credentials file | `./gcp-oauth.keys.json` |
| `TEST_CALENDAR_ID` | All integration tests | Target calendar for test operations | `[email protected]` or `primary` |
| `CLAUDE_API_KEY` | Claude integration tests | Anthropic API access | `sk-ant-api03-...` |
| `OPENAI_API_KEY` | OpenAI integration tests | OpenAI API access | `sk-...` |
| `INVITEE_1` | Attendee tests | Test attendee email | `[email protected]` |
| `INVITEE_2` | Attendee tests | Test attendee email | `[email protected]` |

### Optional Environment Variables

| Variable | Purpose | Default | Notes |
|----------|---------|---------|-------|
| `GOOGLE_ACCOUNT_MODE` | Account mode | `normal` | Use `test` for testing |
| `DEBUG_LLM_INTERACTIONS` | Debug logging | `false` | Set `true` for verbose LLM logs |
| `ANTHROPIC_MODEL` | Claude model | `claude-3-5-haiku-20241022` | Must support MCP |
| `OPENAI_MODEL` | OpenAI model | `gpt-4o-mini` | Must support function calling |

### Complete Setup Example

1. **Create `.env` file in project root:**
```env
# Required for all integration tests
GOOGLE_OAUTH_CREDENTIALS=./gcp-oauth.keys.json
[email protected]

# Required for LLM integration tests
CLAUDE_API_KEY=sk-ant-api03-...
OPENAI_API_KEY=sk-...

# Required for attendee tests
[email protected]
[email protected]

# Optional configurations
GOOGLE_ACCOUNT_MODE=test
DEBUG_LLM_INTERACTIONS=false
ANTHROPIC_MODEL=claude-3-5-haiku-20241022
OPENAI_MODEL=gpt-4o-mini
```

2. **Obtain Google OAuth Credentials:**
   - Go to [Google Cloud Console](https://console.cloud.google.com)
   - Create a new project or select existing
   - Enable Google Calendar API
   - Create OAuth 2.0 credentials (Desktop app type)
   - Download credentials JSON file
   - Save as `gcp-oauth.keys.json` in project root

3. **Authenticate Test Account:**
```bash
# Creates tokens in ~/.config/google-calendar-mcp/tokens.json
npm run dev auth:test
```

4. **Verify Setup:**
```bash
# Check authentication status
npm run dev account:status

# Run a simple integration test
npm run test:integration -- direct-integration.test.ts
```


## Troubleshooting

### Common Issues

**Authentication Errors:**
- **"No credentials found"**: Run `npm run dev auth:test` to authenticate
- **"Token expired"**: Re-authenticate with `npm run dev auth:test`
- **"Invalid credentials"**: Check `GOOGLE_OAUTH_CREDENTIALS` path is correct
- **"Refresh token must be passed"**: Delete tokens and re-authenticate

**API Errors:**
- **Rate limits**: Tests include retry logic, but may still hit limits with frequent runs
- **Calendar not found**: Verify `TEST_CALENDAR_ID` exists and is accessible
- **Permission denied**: Ensure test account has write access to the calendar
- **"Invalid time range"**: Free/busy queries limited to 3 months between timeMin and timeMax

**LLM Integration Errors:**
- **"Invalid API key"**: Check `CLAUDE_API_KEY`/`OPENAI_API_KEY` are set correctly
- **"Insufficient credits"**: LLM tests consume API credits - ensure account has balance
- **"Model not found"**: Verify model name and availability in your API plan
- **"Tool not found" or "Function not supported"**: 
  - Claude: Ensure using Claude 3.5+ model that supports MCP
  - OpenAI: Ensure using GPT-4+ or compatible GPT-3.5-turbo model
- **"Maximum tokens exceeded"**: Some complex tests may hit token limits with verbose models
- **Network timeouts**: LLM tests may take 2-5 minutes due to AI processing time

**Docker Integration Errors:**
- **"Docker not found"**: Ensure Docker is installed and running
- **Port conflicts**: Docker tests use port 3000 - ensure it's available
- **Build failures**: Check Docker build logs for missing dependencies
- **"Cannot connect to Docker daemon"**: Start Docker Desktop or daemon

### Test Data Management

**Calendar Cleanup:**
- Tests attempt to clean up created events automatically
- Failed tests may leave test events in your calendar
- Manually delete events with "Integration Test" or "Test Event" in the title if needed

**Test Isolation:**
- Use a dedicated test calendar (`TEST_CALENDAR_ID`)
- Don't use your personal calendar for testing
- Consider creating a separate Google account for testing

### Performance Considerations

**Test Duration:**
- Unit tests: ~2 seconds
- Direct integration tests: ~30-60 seconds  
- LLM integration tests: ~2-5 minutes (due to AI processing)
- Full test suite: ~5-10 minutes

**Parallel Execution:**
- Unit tests run in parallel by default
- Integration tests run sequentially to avoid API conflicts
- Use `--reporter=verbose` for detailed progress during long test runs

## Development Tips

### Debugging Integration Tests

1. **Enable Debug Logging:**
```bash
# Debug all LLM interactions
export DEBUG_LLM_INTERACTIONS=true

# Debug MCP server
export DEBUG=mcp:*
```

2. **Run Single Test:**
```bash
# Run specific test by name pattern
npm run test:integration -- -t "should handle timezone"
```

3. **Interactive Testing:**
```bash
# Use the dev menu for quick access to test commands
npm run dev
```

### Writing New Integration Tests

1. **Use Test Data Factory:**
```typescript
import { TestDataFactory } from './test-data-factory.js';

const factory = new TestDataFactory();
const testEvent = factory.createTestEvent({
  summary: 'My Test Event',
  start: factory.getTomorrowAt(14, 0),
  end: factory.getTomorrowAt(15, 0)
});
```

2. **Track Created Events:**
```typescript
// Events are automatically tracked for cleanup
const eventId = TestDataFactory.extractEventIdFromResponse(result);
```

3. **LLM Context Logging:**
```typescript
// Wrap LLM operations for automatic error logging
await executeWithContextLogging('Test Name', async () => {
  const response = await llmClient.sendMessage('...');
  // Test assertions
});
```

### Best Practices

1. **Environment Isolation:**
   - Always use `GOOGLE_ACCOUNT_MODE=test` for testing
   - Use a dedicated test calendar, not personal calendar
   - Consider separate Google account for testing

2. **Cost Management:**
   - LLM tests consume API credits
   - Run specific tests during development
   - Use smaller/cheaper models for initial testing

3. **Test Data:**
   - Tests auto-cleanup created events
   - Use unique event titles with timestamps
   - Verify cleanup in afterEach hooks

4. **Debugging Failures:**
   - Check `DEBUG_LLM_INTERACTIONS` output for LLM tests
   - Verify model compatibility for tool/function support
   - Check API quotas and rate limits
```

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

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

// Mock googleapis globally
vi.mock('googleapis', () => ({
  google: {
    calendar: vi.fn(() => ({
      events: {
        list: vi.fn()
      },
      calendarList: {
        get: vi.fn()
      }
    }))
  }
}));

describe('ListEventsHandler JSON String Handling', () => {
  const mockOAuth2Client = {
    getAccessToken: vi.fn().mockResolvedValue({ token: 'mock-token' })
  } as unknown as OAuth2Client;
  
  const handler = new ListEventsHandler();
  let mockCalendar: any;

  beforeEach(() => {
    mockCalendar = {
      events: {
        list: vi.fn().mockResolvedValue({
          data: {
            items: [
              {
                id: 'test-event',
                summary: 'Test Event',
                start: { dateTime: '2025-06-02T10:00:00Z' },
                end: { dateTime: '2025-06-02T11:00:00Z' },
              }
            ]
          }
        })
      },
      calendarList: {
        get: vi.fn().mockResolvedValue({
          data: { timeZone: 'UTC' }
        }),
        list: vi.fn().mockResolvedValue({
          data: {
            items: [
              { id: 'primary', summary: 'Primary Calendar' },
              { id: '[email protected]', summary: 'Work Calendar' },
              { id: '[email protected]', summary: 'Personal Calendar' }
            ]
          }
        })
      }
    };
    vi.mocked(google.calendar).mockReturnValue(mockCalendar);
  });

  // Mock fetch for batch requests
  global.fetch = vi.fn().mockResolvedValue({
    ok: true,
    status: 200,
    text: () => Promise.resolve(`--batch_boundary
Content-Type: application/http
Content-ID: <item1>

HTTP/1.1 200 OK
Content-Type: application/json

{"items": [{"id": "test-event", "summary": "Test Event", "start": {"dateTime": "2025-06-02T10:00:00Z"}, "end": {"dateTime": "2025-06-02T11:00:00Z"}}]}

--batch_boundary--`)
  });

  it('should handle single calendar ID as string', async () => {
    const args = {
      calendarId: 'primary',
      timeMin: '2025-06-02T00:00:00Z',
      timeMax: '2025-06-09T23:59:59Z'
    };

    const result = await handler.runTool(args, mockOAuth2Client);
    expect(result.content).toHaveLength(1);
    expect(result.content[0].type).toBe('text');
    const response = JSON.parse((result.content[0] as any).text);
    expect(response.events).toBeDefined();
    expect(response.totalCount).toBeGreaterThanOrEqual(0);
  });

  it('should handle multiple calendar IDs as array', async () => {
    const args = {
      calendarId: ['primary', '[email protected]'],
      timeMin: '2025-06-02T00:00:00Z',
      timeMax: '2025-06-09T23:59:59Z'
    };

    const result = await handler.runTool(args, mockOAuth2Client);
    expect(result.content).toHaveLength(1);
    expect(result.content[0].type).toBe('text');
    const response = JSON.parse((result.content[0] as any).text);
    expect(response.events).toBeDefined();
    expect(response.totalCount).toBeGreaterThanOrEqual(0);
  });

  it('should handle calendar IDs passed as JSON string', async () => {
    // This simulates the problematic case from the user
    const args = {
      calendarId: '["primary", "[email protected]"]',
      timeMin: '2025-06-02T00:00:00Z',
      timeMax: '2025-06-09T23:59:59Z'
    };

    // This would be parsed by the Zod transform before reaching the handler
    // For testing, we'll manually simulate what the transform should do
    let processedArgs = { ...args };
    if (typeof args.calendarId === 'string' && args.calendarId.startsWith('[')) {
      processedArgs.calendarId = JSON.parse(args.calendarId);
    }

    const result = await handler.runTool(processedArgs, mockOAuth2Client);
    expect(result.content).toHaveLength(1);
    expect(result.content[0].type).toBe('text');
    const response = JSON.parse((result.content[0] as any).text);
    expect(response.events).toBeDefined();
    expect(response.totalCount).toBeGreaterThanOrEqual(0);
    expect(response.calendars).toEqual(['primary', '[email protected]']);
  });
});

describe('ListEventsHandler - Timezone Handling', () => {
  let handler: ListEventsHandler;
  let mockOAuth2Client: OAuth2Client;
  let mockCalendar: any;

  beforeEach(() => {
    handler = new ListEventsHandler();
    mockOAuth2Client = {} as OAuth2Client;
    mockCalendar = {
      events: {
        list: vi.fn()
      },
      calendarList: {
        get: vi.fn(),
        list: vi.fn().mockResolvedValue({
          data: {
            items: [
              { id: 'primary', summary: 'Primary Calendar' },
              { id: '[email protected]', summary: 'Work Calendar' }
            ]
          }
        })
      }
    };
    vi.mocked(google.calendar).mockReturnValue(mockCalendar);
  });

  describe('convertToRFC3339 timezone interpretation', () => {
    it('should correctly convert timezone-naive datetime to Los Angeles time', () => {
      // Test the core issue: timezone-naive datetime should be interpreted in the target timezone
      const datetime = '2025-01-01T10:00:00';
      const timezone = 'America/Los_Angeles';
      
      const result = convertToRFC3339(datetime, timezone);
      
      // In January 2025, Los Angeles is UTC-8 (PST)
      // 10:00 AM PST = 18:00 UTC
      // The result should be '2025-01-01T18:00:00Z'
      expect(result).toBe('2025-01-01T18:00:00Z');
    });

    it('should correctly convert timezone-naive datetime to New York time', () => {
      const datetime = '2025-01-01T10:00:00';
      const timezone = 'America/New_York';
      
      const result = convertToRFC3339(datetime, timezone);
      
      // In January 2025, New York is UTC-5 (EST)
      // 10:00 AM EST = 15:00 UTC
      expect(result).toBe('2025-01-01T15:00:00Z');
    });

    it('should correctly convert timezone-naive datetime to London time', () => {
      const datetime = '2025-01-01T10:00:00';
      const timezone = 'Europe/London';
      
      const result = convertToRFC3339(datetime, timezone);
      
      // In January 2025, London is UTC+0 (GMT)
      // 10:00 AM GMT = 10:00 UTC
      expect(result).toBe('2025-01-01T10:00:00Z');
    });

    it('should handle DST transitions correctly', () => {
      // Test during DST period
      const datetime = '2025-07-01T10:00:00';
      const timezone = 'America/Los_Angeles';
      
      const result = convertToRFC3339(datetime, timezone);
      
      // In July 2025, Los Angeles is UTC-7 (PDT)
      // 10:00 AM PDT = 17:00 UTC
      expect(result).toBe('2025-07-01T17:00:00Z');
    });

    it('should leave timezone-aware datetime unchanged', () => {
      const datetime = '2025-01-01T10:00:00-08:00';
      const timezone = 'America/Los_Angeles';
      
      const result = convertToRFC3339(datetime, timezone);
      
      // Should remain unchanged since it already has timezone info
      expect(result).toBe('2025-01-01T10:00:00-08:00');
    });
  });

  describe('ListEventsHandler timezone parameter usage', () => {
    beforeEach(() => {
      // Mock successful calendar list response
      mockCalendar.calendarList.get.mockResolvedValue({
        data: { timeZone: 'UTC' }
      });
      
      // Mock successful events list response
      mockCalendar.events.list.mockResolvedValue({
        data: { items: [] }
      });
    });

    it('should use timeZone parameter to interpret timezone-naive timeMin/timeMax', async () => {
      const args = {
        calendarId: 'primary',
        timeMin: '2025-01-01T10:00:00',
        timeMax: '2025-01-01T18:00:00',
        timeZone: 'America/Los_Angeles'
      };

      await handler.runTool(args, mockOAuth2Client);

      // Verify that the calendar.events.list was called with correctly converted times
      expect(mockCalendar.events.list).toHaveBeenCalledWith({
        calendarId: 'primary',
        timeMin: '2025-01-01T18:00:00Z', // 10:00 AM PST = 18:00 UTC
        timeMax: '2025-01-02T02:00:00Z', // 18:00 PM PST = 02:00 UTC next day
        singleEvents: true,
        orderBy: 'startTime'
      });
    });

    it('should preserve timezone-aware timeMin/timeMax regardless of timeZone parameter', async () => {
      const args = {
        calendarId: 'primary',
        timeMin: '2025-01-01T10:00:00-08:00',
        timeMax: '2025-01-01T18:00:00-08:00',
        timeZone: 'America/New_York' // Different timezone, should be ignored
      };

      await handler.runTool(args, mockOAuth2Client);

      // Verify that the original timezone-aware times are preserved
      expect(mockCalendar.events.list).toHaveBeenCalledWith({
        calendarId: 'primary',
        timeMin: '2025-01-01T10:00:00-08:00',
        timeMax: '2025-01-01T18:00:00-08:00',
        singleEvents: true,
        orderBy: 'startTime'
      });
    });

    it('should fall back to calendar timezone when timeZone parameter not provided', async () => {
      // Mock calendar with Los Angeles timezone
      mockCalendar.calendarList.get.mockResolvedValue({
        data: { timeZone: 'America/Los_Angeles' }
      });

      const args = {
        calendarId: 'primary',
        timeMin: '2025-01-01T10:00:00',
        timeMax: '2025-01-01T18:00:00'
        // No timeZone parameter
      };

      await handler.runTool(args, mockOAuth2Client);

      // Verify that the calendar's timezone is used for conversion
      expect(mockCalendar.events.list).toHaveBeenCalledWith({
        calendarId: 'primary',
        timeMin: '2025-01-01T18:00:00Z', // 10:00 AM PST = 18:00 UTC
        timeMax: '2025-01-02T02:00:00Z', // 18:00 PM PST = 02:00 UTC next day
        singleEvents: true,
        orderBy: 'startTime'
      });
    });

    it('should handle UTC timezone correctly', async () => {
      const args = {
        calendarId: 'primary',
        timeMin: '2025-01-01T10:00:00',
        timeMax: '2025-01-01T18:00:00',
        timeZone: 'UTC'
      };

      await handler.runTool(args, mockOAuth2Client);

      // Verify that UTC times are handled correctly
      expect(mockCalendar.events.list).toHaveBeenCalledWith({
        calendarId: 'primary',
        timeMin: '2025-01-01T10:00:00Z',
        timeMax: '2025-01-01T18:00:00Z',
        singleEvents: true,
        orderBy: 'startTime'
      });
    });
  });
});
```

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

```typescript
import { OAuth2Client } from "google-auth-library";
import { google, calendar_v3 } from "googleapis";
import {
  ConflictCheckResult,
  InternalConflictInfo,
  InternalDuplicateInfo,
  ConflictDetectionOptions
} from "./types.js";
import { EventSimilarityChecker } from "./EventSimilarityChecker.js";
import { ConflictAnalyzer } from "./ConflictAnalyzer.js";
import { CONFLICT_DETECTION_CONFIG } from "./config.js";
import { getEventUrl } from "../../handlers/utils.js";
import { convertToRFC3339 } from "../../handlers/utils/datetime.js";

/**
 * Service for detecting event conflicts and duplicates.
 * 
 * IMPORTANT: This service relies on Google Calendar's list API to find existing events.
 * Due to eventual consistency in Google Calendar, recently created events may not
 * immediately appear in list queries. This is a known limitation of the Google Calendar API
 * and affects duplicate detection for events created in quick succession.
 * 
 * In real-world usage, this is rarely an issue as there's natural time between event creation.
 */
export class ConflictDetectionService {
  private similarityChecker: EventSimilarityChecker;
  private conflictAnalyzer: ConflictAnalyzer;
  
  constructor() {
    this.similarityChecker = new EventSimilarityChecker();
    this.conflictAnalyzer = new ConflictAnalyzer();
  }

  /**
   * Check for conflicts and duplicates when creating or updating an event
   */
  async checkConflicts(
    oauth2Client: OAuth2Client,
    event: calendar_v3.Schema$Event,
    calendarId: string,
    options: ConflictDetectionOptions = {}
  ): Promise<ConflictCheckResult> {
    const {
      checkDuplicates = true,
      checkConflicts = true,
      calendarsToCheck = [calendarId],
      duplicateSimilarityThreshold = CONFLICT_DETECTION_CONFIG.DEFAULT_DUPLICATE_THRESHOLD,
      includeDeclinedEvents = false
    } = options;

    const result: ConflictCheckResult = {
      hasConflicts: false,
      conflicts: [],
      duplicates: []
    };

    if (!event.start || !event.end) {
      return result;
    }

    // Get the time range for checking
    let timeMin = event.start.dateTime || event.start.date;
    let timeMax = event.end.dateTime || event.end.date;

    if (!timeMin || !timeMax) {
      return result;
    }

    // Extract timezone if present (prefer start time's timezone)
    const timezone = event.start.timeZone || event.end.timeZone;
    
    
    // The Google Calendar API requires RFC3339 format for timeMin/timeMax
    // If we have timezone-naive datetimes with a timezone field, convert them to proper RFC3339
    // Check for minus but exclude the date separator (e.g., 2025-09-05)
    const needsConversion = timezone && timeMin && 
      !timeMin.includes('Z') && 
      !timeMin.includes('+') && 
      !timeMin.substring(10).includes('-'); // Only check for minus after the date part
      
    if (needsConversion) {
      timeMin = convertToRFC3339(timeMin, timezone);
      timeMax = convertToRFC3339(timeMax, timezone);
    }
    
    
    // Use the exact time range provided for searching
    // This ensures duplicate detection only flags events that actually overlap
    const searchTimeMin = timeMin;
    const searchTimeMax = timeMax;

    // Check each calendar
    for (const checkCalendarId of calendarsToCheck) {
      try {
        // Get events in the search time range, passing timezone for proper interpretation
        const events = await this.getEventsInTimeRange(
          oauth2Client,
          checkCalendarId,
          searchTimeMin,
          searchTimeMax,
          timezone || undefined
        );

        // Check for duplicates
        if (checkDuplicates) {
          const duplicates = this.findDuplicates(
            event,
            events,
            checkCalendarId,
            duplicateSimilarityThreshold
          );
          result.duplicates.push(...duplicates);
        }

        // Check for conflicts
        if (checkConflicts) {
          const conflicts = this.findConflicts(
            event,
            events,
            checkCalendarId,
            includeDeclinedEvents
          );
          result.conflicts.push(...conflicts);
        }
      } catch (error) {
        // If we can't access a calendar, skip it silently
        // Errors are expected for calendars without access permissions
      }
    }

    result.hasConflicts = result.conflicts.length > 0 || result.duplicates.length > 0;
    return result;
  }

  /**
   * Get events in a specific time range from a calendar
   */
  private async getEventsInTimeRange(
    oauth2Client: OAuth2Client,
    calendarId: string,
    timeMin: string,
    timeMax: string,
    timeZone?: string
  ): Promise<calendar_v3.Schema$Event[]> {
    // Fetch from API
    const calendar = google.calendar({ version: "v3", auth: oauth2Client });
    
    // Build list parameters
    const listParams: any = {
      calendarId,
      timeMin,
      timeMax,
      singleEvents: true,
      orderBy: 'startTime',
      maxResults: 250
    };
    
    // The Google Calendar API accepts both:
    // 1. Timezone-aware datetimes (with Z or offset)
    // 2. Timezone-naive datetimes with a timeZone parameter
    // We pass the timeZone parameter when available for consistency
    if (timeZone) {
      listParams.timeZone = timeZone;
    }
    
    
    // Use exact time range without extension to avoid false positives
    const response = await calendar.events.list(listParams);

    const events = response?.data?.items || [];
    
    return events;
  }

  /**
   * Find duplicate events based on similarity
   */
  private findDuplicates(
    newEvent: calendar_v3.Schema$Event,
    existingEvents: calendar_v3.Schema$Event[],
    calendarId: string,
    threshold: number
  ): InternalDuplicateInfo[] {
    const duplicates: InternalDuplicateInfo[] = [];


    for (const existingEvent of existingEvents) {
      // Skip if it's the same event (for updates)
      if (existingEvent.id === newEvent.id) continue;
      
      // Skip cancelled events
      if (existingEvent.status === 'cancelled') continue;

      const similarity = this.similarityChecker.checkSimilarity(newEvent, existingEvent);
      
      
      if (similarity >= threshold) {
        duplicates.push({
          event: {
            id: existingEvent.id!,
            title: existingEvent.summary || 'Untitled Event',
            url: getEventUrl(existingEvent, calendarId) || undefined,
            similarity: Math.round(similarity * 100) / 100
          },
          fullEvent: existingEvent,
          calendarId: calendarId,
          suggestion: similarity >= CONFLICT_DETECTION_CONFIG.DUPLICATE_THRESHOLDS.BLOCKING
            ? 'This appears to be a duplicate. Consider updating the existing event instead.'
            : 'This event is very similar to an existing one. Is this intentional?'
        });
      }
    }


    return duplicates;
  }

  /**
   * Find conflicting events based on time overlap
   */
  private findConflicts(
    newEvent: calendar_v3.Schema$Event,
    existingEvents: calendar_v3.Schema$Event[],
    calendarId: string,
    includeDeclinedEvents: boolean
  ): InternalConflictInfo[] {
    const conflicts: InternalConflictInfo[] = [];
    const overlappingEvents = this.conflictAnalyzer.findOverlappingEvents(existingEvents, newEvent);

    for (const conflictingEvent of overlappingEvents) {
      // Skip declined events if configured
      if (!includeDeclinedEvents && this.isEventDeclined(conflictingEvent)) {
        continue;
      }

      const overlap = this.conflictAnalyzer.analyzeOverlap(newEvent, conflictingEvent);
      
      if (overlap.hasOverlap) {
        conflicts.push({
          type: 'overlap',
          calendar: calendarId,
          event: {
            id: conflictingEvent.id!,
            title: conflictingEvent.summary || 'Untitled Event',
            url: getEventUrl(conflictingEvent, calendarId) || undefined,
            start: conflictingEvent.start?.dateTime || conflictingEvent.start?.date || undefined,
            end: conflictingEvent.end?.dateTime || conflictingEvent.end?.date || undefined
          },
          fullEvent: conflictingEvent,
          overlap: {
            duration: overlap.duration!,
            percentage: overlap.percentage!,
            startTime: overlap.startTime!,
            endTime: overlap.endTime!
          }
        });
      }
    }

    return conflicts;
  }

  /**
   * Check if the current user has declined an event
   */
  private isEventDeclined(_event: calendar_v3.Schema$Event): boolean {
    // For now, we'll skip this check since we don't have easy access to the user's email
    // This could be enhanced later by passing the user email through the service
    return false;
  }

  /**
   * Check for conflicts using free/busy data (alternative method)
   */
  async checkConflictsWithFreeBusy(
    oauth2Client: OAuth2Client,
    eventToCheck: calendar_v3.Schema$Event,
    calendarsToCheck: string[]
  ): Promise<InternalConflictInfo[]> {
    const conflicts: InternalConflictInfo[] = [];
    
    if (!eventToCheck.start || !eventToCheck.end) return conflicts;
    
    const timeMin = eventToCheck.start.dateTime || eventToCheck.start.date;
    const timeMax = eventToCheck.end.dateTime || eventToCheck.end.date;
    
    if (!timeMin || !timeMax) return conflicts;

    const calendar = google.calendar({ version: "v3", auth: oauth2Client });
    
    try {
      const freeBusyResponse = await calendar.freebusy.query({
        requestBody: {
          timeMin,
          timeMax,
          items: calendarsToCheck.map(id => ({ id }))
        }
      });

      for (const [calendarId, calendarInfo] of Object.entries(freeBusyResponse.data.calendars || {})) {
        if (calendarInfo.busy && calendarInfo.busy.length > 0) {
          for (const busySlot of calendarInfo.busy) {
            if (this.conflictAnalyzer.checkBusyConflict(eventToCheck, busySlot)) {
              conflicts.push({
                type: 'overlap',
                calendar: calendarId,
                event: {
                  id: 'busy-time',
                  title: 'Busy (details unavailable)',
                  start: busySlot.start || undefined,
                  end: busySlot.end || undefined
                }
              });
            }
          }
        }
      }
    } catch (error) {
      console.error('Failed to check free/busy:', error);
    }

    return conflicts;
  }
}
```

--------------------------------------------------------------------------------
/src/tests/integration/test-data-factory.ts:
--------------------------------------------------------------------------------

```typescript
// Test data factory utilities for integration tests

export interface TestEvent {
  id?: string;
  summary: string;
  description?: string;
  start: string;
  end: string;
  timeZone?: string; // Optional for all-day events
  location?: string;
  attendees?: Array<{ email: string }>;
  colorId?: string;
  reminders?: {
    useDefault: boolean;
    overrides?: Array<{ method: "email" | "popup"; minutes: number }>;
  };
  recurrence?: string[];
  modificationScope?: "thisAndFollowing" | "all" | "thisEventOnly";
  originalStartTime?: string;
  futureStartDate?: string;
  calendarId?: string;
  sendUpdates?: "all" | "externalOnly" | "none";
}

export interface PerformanceMetric {
  operation: string;
  startTime: number;
  endTime: number;
  duration: number;
  success: boolean;
  error?: string;
}

export class TestDataFactory {
  private static readonly TEST_CALENDAR_ID = process.env.TEST_CALENDAR_ID || 'primary';
  
  private createdEventIds: string[] = [];
  private performanceMetrics: PerformanceMetric[] = [];

  static getTestCalendarId(): string {
    return TestDataFactory.TEST_CALENDAR_ID;
  }

  // Helper method to format dates in RFC3339 format without milliseconds
  // For events with a timeZone field, use timezone-naive format to avoid conflicts
  public static formatDateTimeRFC3339(date: Date): string {
    const isoString = date.toISOString();
    // Return timezone-naive format (without Z suffix) to work better with timeZone field
    return isoString.replace(/\.\d{3}Z$/, '');
  }

  // Helper method to format dates in RFC3339 format with timezone (for search operations)
  public static formatDateTimeRFC3339WithTimezone(date: Date): string {
    return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
  }

  // Event data generators
  static createSingleEvent(overrides: Partial<TestEvent> = {}): TestEvent {
    const now = new Date();
    const start = new Date(now.getTime() + 2 * 60 * 60 * 1000); // 2 hours from now
    const end = new Date(start.getTime() + 60 * 60 * 1000); // 1 hour duration

    return {
      summary: 'Test Integration Event',
      description: 'Created by integration test suite',
      start: this.formatDateTimeRFC3339(start),
      end: this.formatDateTimeRFC3339(end),
      timeZone: 'America/Los_Angeles',
      location: 'Test Conference Room',
      reminders: {
        useDefault: false,
        overrides: [{ method: 'popup', minutes: 15 }]
      },
      ...overrides
    };
  }

  static createAllDayEvent(overrides: Partial<TestEvent> = {}): TestEvent {
    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);
    
    const dayAfter = new Date(tomorrow);
    dayAfter.setDate(dayAfter.getDate() + 1);

    // For all-day events, use date-only format (YYYY-MM-DD)
    const startDate = tomorrow.toISOString().split('T')[0];
    const endDate = dayAfter.toISOString().split('T')[0];

    return {
      summary: 'Test All-Day Event',
      description: 'All-day test event',
      start: startDate,
      end: endDate,
      // Note: timeZone is not used for all-day events (they're date-only)
      ...overrides
    };
  }

  static createRecurringEvent(overrides: Partial<TestEvent> = {}): TestEvent {
    const start = new Date();
    start.setDate(start.getDate() + 1); // Tomorrow
    start.setHours(10, 0, 0, 0); // 10 AM
    
    const end = new Date(start);
    end.setHours(11, 0, 0, 0); // 11 AM

    return {
      summary: 'Test Recurring Meeting',
      description: 'Weekly recurring test meeting',
      start: this.formatDateTimeRFC3339(start),
      end: this.formatDateTimeRFC3339(end),
      timeZone: 'America/Los_Angeles',
      location: 'Recurring Meeting Room',
      recurrence: ['RRULE:FREQ=WEEKLY;COUNT=5'], // 5 weeks
      reminders: {
        useDefault: false,
        overrides: [{ method: 'email', minutes: 1440 }] // 1 day before
      },
      ...overrides
    };
  }

  static createEventWithAttendees(overrides: Partial<TestEvent> = {}): TestEvent {
    const invitee1 = process.env.INVITEE_1;
    const invitee2 = process.env.INVITEE_2;
    
    if (!invitee1 || !invitee2) {
      throw new Error('INVITEE_1 and INVITEE_2 environment variables are required for creating events with attendees');
    }
    
    return this.createSingleEvent({
      summary: 'Test Meeting with Attendees',
      attendees: [
        { email: invitee1 },
        { email: invitee2 }
      ],
      ...overrides
    });
  }

  static createColoredEvent(colorId: string, overrides: Partial<TestEvent> = {}): TestEvent {
    return this.createSingleEvent({
      summary: `Test Event - Color ${colorId}`,
      colorId,
      ...overrides
    });
  }

  // Time range generators
  static getTimeRanges() {
    const now = new Date();
    
    return {
      // Past week
      pastWeek: {
        timeMin: this.formatDateTimeRFC3339WithTimezone(new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)),
        timeMax: this.formatDateTimeRFC3339WithTimezone(now)
      },
      // Next week
      nextWeek: {
        timeMin: this.formatDateTimeRFC3339WithTimezone(now),
        timeMax: this.formatDateTimeRFC3339WithTimezone(new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000))
      },
      // Next month
      nextMonth: {
        timeMin: this.formatDateTimeRFC3339WithTimezone(now),
        timeMax: this.formatDateTimeRFC3339WithTimezone(new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000))
      },
      // Large range (3 months)
      threeMonths: {
        timeMin: this.formatDateTimeRFC3339WithTimezone(now),
        timeMax: this.formatDateTimeRFC3339WithTimezone(new Date(now.getTime() + 90 * 24 * 60 * 60 * 1000))
      }
    };
  }

  // Performance tracking
  startTimer(_operation: string): number {
    return Date.now();
  }

  endTimer(operation: string, startTime: number, success: boolean, error?: string): void {
    const endTime = Date.now();
    const duration = endTime - startTime;
    
    this.performanceMetrics.push({
      operation,
      startTime,
      endTime,
      duration,
      success,
      error
    });
  }

  getPerformanceMetrics(): PerformanceMetric[] {
    return [...this.performanceMetrics];
  }

  clearPerformanceMetrics(): void {
    this.performanceMetrics = [];
  }

  // Event tracking for cleanup
  addCreatedEventId(eventId: string): void {
    this.createdEventIds.push(eventId);
  }

  getCreatedEventIds(): string[] {
    return [...this.createdEventIds];
  }

  clearCreatedEventIds(): void {
    this.createdEventIds = [];
  }

  // Search queries
  static getSearchQueries() {
    return [
      'Test Integration',
      'meeting',
      'recurring',
      'attendees',
      'Conference Room',
      'nonexistent_query_should_return_empty'
    ];
  }

  // Validation helpers
  static validateEventResponse(response: any): boolean {
    if (!response || !response.content || !Array.isArray(response.content)) {
      return false;
    }
    
    const text = response.content[0]?.text;
    // Accept empty strings for search operations - they indicate "no results found"
    return typeof text === 'string';
  }

  static extractEventIdFromResponse(response: any): string | null {
    const text = response.content[0]?.text;
    if (!text) return null;
    
    // Try to parse as JSON first (v2.0 structured response)
    try {
      const parsed = JSON.parse(text);
      if (parsed.event?.id) {
        return parsed.event.id;
      }
    } catch {
      // Fall back to legacy text parsing
    }
    
    // Look for various event ID patterns in the response (legacy)
    // Google Calendar event IDs can contain letters, numbers, underscores, and special characters
    const patterns = [
      /Event created: .* \(([^)]+)\)/, // Legacy format - Match anything within parentheses after "Event created:"
      /Event updated: .* \(([^)]+)\)/, // Legacy format - Match anything within parentheses after "Event updated:"
      /✅ Event created successfully[\s\S]*?([^\s\(]+) \(([^)]+)\)/, // New format - Extract ID from parentheses in event details
      /✅ Event updated successfully[\s\S]*?([^\s\(]+) \(([^)]+)\)/, // New format - Extract ID from parentheses in event details
      /Event ID: ([^\s]+)/, // Match non-whitespace characters after "Event ID:"
      /Created event: .* \(ID: ([^)]+)\)/, // Match anything within parentheses after "ID:"
      /\(([[email protected]]{10,})\)/, // Specific pattern for Google Calendar IDs with common characters
    ];
    
    for (const pattern of patterns) {
      const match = text.match(pattern);
      if (match) {
        // For patterns with multiple capture groups, we want the event ID
        // which is typically in the last parentheses
        let eventId = match[match.length - 1] || match[1];
        if (eventId) {
          // Clean up the captured ID (trim whitespace)
          eventId = eventId.trim();
          // Basic validation - should be at least 10 characters
          if (eventId.length >= 10) {
            return eventId;
          }
        }
      }
    }
    
    return null;
  }

  static extractAllEventIds(response: any): string[] {
    const text = response.content[0]?.text;
    if (!text) return [];
    
    const eventIds: string[] = [];
    
    // Look for event IDs in list format - they appear in parentheses after event titles
    // Pattern: anything that looks like an event ID in parentheses
    const pattern = /\(([[email protected]]{10,})\)/g;
    
    let match;
    while ((match = pattern.exec(text)) !== null) {
      const eventId = match[1].trim();
      // Basic validation - should be at least 10 characters and not contain spaces
      if (eventId.length >= 10 && !eventId.includes(' ')) {
        eventIds.push(eventId);
      }
    }
    
    // Also look for Event ID: patterns
    const idPattern = /Event ID:\s*([[email protected]]+)/g;
    while ((match = idPattern.exec(text)) !== null) {
      const eventId = match[1].trim();
      if (eventId.length >= 10 && !eventIds.includes(eventId)) {
        eventIds.push(eventId);
      }
    }
    
    return eventIds;
  }

  // Error simulation helpers
  static getInvalidTestData() {
    return {
      invalidCalendarId: 'invalid_calendar_id',
      invalidEventId: 'invalid_event_id',
      invalidTimeFormat: '2024-13-45T25:99:99Z',
      invalidTimezone: 'Invalid/Timezone',
      invalidEmail: 'not-an-email',
      invalidColorId: '999',
      malformedRecurrence: ['INVALID:RRULE'],
      futureDateInPast: '2020-01-01T10:00:00Z'
    };
  }
}
```

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

```typescript
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { OAuth2Client } from "google-auth-library";
import { UpdateEventInput } from "../../tools/registry.js";
import { BaseToolHandler } from "./BaseToolHandler.js";
import { calendar_v3 } from 'googleapis';
import { RecurringEventHelpers, RecurringEventError, RECURRING_EVENT_ERRORS } from './RecurringEventHelpers.js';
import { ConflictDetectionService } from "../../services/conflict-detection/index.js";
import { createTimeObject } from "../utils/datetime.js";
import { 
    createStructuredResponse, 
    convertConflictsToStructured,
    createWarningsArray
} from "../../utils/response-builder.js";
import { 
    UpdateEventResponse,
    convertGoogleEventToStructured 
} from "../../types/structured-responses.js";

export class UpdateEventHandler extends BaseToolHandler {
    private conflictDetectionService: ConflictDetectionService;
    
    constructor() {
        super();
        this.conflictDetectionService = new ConflictDetectionService();
    }
    
    async runTool(args: any, oauth2Client: OAuth2Client): Promise<CallToolResult> {
        const validArgs = args as UpdateEventInput;

        // Check for conflicts if enabled
        let conflicts = null;
        if (validArgs.checkConflicts !== false && (validArgs.start || validArgs.end)) {
            // Get the existing event to merge with updates
            const calendar = this.getCalendar(oauth2Client);
            const existingEvent = await calendar.events.get({
                calendarId: validArgs.calendarId,
                eventId: validArgs.eventId
            });

            if (!existingEvent.data) {
                throw new Error('Event not found');
            }

            // Create updated event object for conflict checking
            const timezone = validArgs.timeZone || await this.getCalendarTimezone(oauth2Client, validArgs.calendarId);
            const eventToCheck: calendar_v3.Schema$Event = {
                ...existingEvent.data,
                id: validArgs.eventId,
                summary: validArgs.summary || existingEvent.data.summary,
                description: validArgs.description || existingEvent.data.description,
                start: validArgs.start ? createTimeObject(validArgs.start, timezone) : existingEvent.data.start,
                end: validArgs.end ? createTimeObject(validArgs.end, timezone) : existingEvent.data.end,
                location: validArgs.location || existingEvent.data.location,
            };

            // Check for conflicts
            conflicts = await this.conflictDetectionService.checkConflicts(
                oauth2Client,
                eventToCheck,
                validArgs.calendarId,
                {
                    checkDuplicates: false, // Don't check duplicates for updates
                    checkConflicts: true,
                    calendarsToCheck: validArgs.calendarsToCheck || [validArgs.calendarId]
                }
            );
        }

        // Update the event
        const event = await this.updateEventWithScope(oauth2Client, validArgs);

        // Create structured response
        const response: UpdateEventResponse = {
            event: convertGoogleEventToStructured(event, validArgs.calendarId)
        };
        
        // Add conflict information if present
        if (conflicts && conflicts.hasConflicts) {
            const structuredConflicts = convertConflictsToStructured(conflicts);
            if (structuredConflicts.conflicts) {
                response.conflicts = structuredConflicts.conflicts;
            }
            response.warnings = createWarningsArray(conflicts);
        }
        
        return createStructuredResponse(response);
    }

    private async updateEventWithScope(
        client: OAuth2Client,
        args: UpdateEventInput
    ): Promise<calendar_v3.Schema$Event> {
        try {
            const calendar = this.getCalendar(client);
            const helpers = new RecurringEventHelpers(calendar);
            
            // Get calendar's default timezone if not provided
            const defaultTimeZone = await this.getCalendarTimezone(client, args.calendarId);
            
            // Detect event type and validate scope usage
            const eventType = await helpers.detectEventType(args.eventId, args.calendarId);
            
            if (args.modificationScope && args.modificationScope !== 'all' && eventType !== 'recurring') {
                throw new RecurringEventError(
                    'Scope other than "all" only applies to recurring events',
                    RECURRING_EVENT_ERRORS.NON_RECURRING_SCOPE
                );
            }
            
            switch (args.modificationScope) {
                case 'thisEventOnly':
                    return this.updateSingleInstance(helpers, args, defaultTimeZone);
                case 'all':
                case undefined:
                    return this.updateAllInstances(helpers, args, defaultTimeZone);
                case 'thisAndFollowing':
                    return this.updateFutureInstances(helpers, args, defaultTimeZone);
                default:
                    throw new RecurringEventError(
                        `Invalid modification scope: ${args.modificationScope}`,
                        RECURRING_EVENT_ERRORS.INVALID_SCOPE
                    );
            }
        } catch (error) {
            if (error instanceof RecurringEventError) {
                throw error;
            }
            throw this.handleGoogleApiError(error);
        }
    }

    private async updateSingleInstance(
        helpers: RecurringEventHelpers,
        args: UpdateEventInput,
        defaultTimeZone: string
    ): Promise<calendar_v3.Schema$Event> {
        if (!args.originalStartTime) {
            throw new RecurringEventError(
                'originalStartTime is required for single instance updates',
                RECURRING_EVENT_ERRORS.MISSING_ORIGINAL_TIME
            );
        }

        const calendar = helpers.getCalendar();
        const instanceId = helpers.formatInstanceId(args.eventId, args.originalStartTime);

        const requestBody = helpers.buildUpdateRequestBody(args, defaultTimeZone);
        const conferenceDataVersion = requestBody.conferenceData !== undefined ? 1 : undefined;
        const supportsAttachments = requestBody.attachments !== undefined ? true : undefined;

        const response = await calendar.events.patch({
            calendarId: args.calendarId,
            eventId: instanceId,
            requestBody,
            ...(conferenceDataVersion && { conferenceDataVersion }),
            ...(supportsAttachments && { supportsAttachments })
        });

        if (!response.data) throw new Error('Failed to update event instance');
        return response.data;
    }

    private async updateAllInstances(
        helpers: RecurringEventHelpers,
        args: UpdateEventInput,
        defaultTimeZone: string
    ): Promise<calendar_v3.Schema$Event> {
        const calendar = helpers.getCalendar();

        const requestBody = helpers.buildUpdateRequestBody(args, defaultTimeZone);
        const conferenceDataVersion = requestBody.conferenceData !== undefined ? 1 : undefined;
        const supportsAttachments = requestBody.attachments !== undefined ? true : undefined;

        const response = await calendar.events.patch({
            calendarId: args.calendarId,
            eventId: args.eventId,
            requestBody,
            ...(conferenceDataVersion && { conferenceDataVersion }),
            ...(supportsAttachments && { supportsAttachments })
        });

        if (!response.data) throw new Error('Failed to update event');
        return response.data;
    }

    private async updateFutureInstances(
        helpers: RecurringEventHelpers,
        args: UpdateEventInput,
        defaultTimeZone: string
    ): Promise<calendar_v3.Schema$Event> {
        if (!args.futureStartDate) {
            throw new RecurringEventError(
                'futureStartDate is required for future instance updates',
                RECURRING_EVENT_ERRORS.MISSING_FUTURE_DATE
            );
        }

        const calendar = helpers.getCalendar();
        const effectiveTimeZone = args.timeZone || defaultTimeZone;

        // 1. Get original event
        const originalResponse = await calendar.events.get({
            calendarId: args.calendarId,
            eventId: args.eventId
        });
        const originalEvent = originalResponse.data;

        if (!originalEvent.recurrence) {
            throw new Error('Event does not have recurrence rules');
        }

        // 2. Calculate UNTIL date and update original event
        const untilDate = helpers.calculateUntilDate(args.futureStartDate);
        const updatedRecurrence = helpers.updateRecurrenceWithUntil(originalEvent.recurrence, untilDate);

        await calendar.events.patch({
            calendarId: args.calendarId,
            eventId: args.eventId,
            requestBody: { recurrence: updatedRecurrence }
        });

        // 3. Create new recurring event starting from future date
        const requestBody = helpers.buildUpdateRequestBody(args, defaultTimeZone);
        
        // Calculate end time if start time is changing
        let endTime = args.end;
        if (args.start || args.futureStartDate) {
            const newStartTime = args.start || args.futureStartDate;
            endTime = endTime || helpers.calculateEndTime(newStartTime, originalEvent);
        }

        const newEvent = {
            ...helpers.cleanEventForDuplication(originalEvent),
            ...requestBody,
            start: { 
                dateTime: args.start || args.futureStartDate, 
                timeZone: effectiveTimeZone 
            },
            end: { 
                dateTime: endTime, 
                timeZone: effectiveTimeZone 
            }
        };

        const conferenceDataVersion = newEvent.conferenceData !== undefined ? 1 : undefined;
        const supportsAttachments = newEvent.attachments !== undefined ? true : undefined;

        const response = await calendar.events.insert({
            calendarId: args.calendarId,
            requestBody: newEvent,
            ...(conferenceDataVersion && { conferenceDataVersion }),
            ...(supportsAttachments && { supportsAttachments })
        });

        if (!response.data) throw new Error('Failed to create new recurring event');
        return response.data;
    }

}

```

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

```typescript
import { calendar_v3 } from "googleapis";
import { ConflictCheckResult } from "../services/conflict-detection/types.js";

/**
 * Generates a Google Calendar event view URL
 */
export function generateEventUrl(calendarId: string, eventId: string): string {
    const encodedCalendarId = encodeURIComponent(calendarId);
    const encodedEventId = encodeURIComponent(eventId);
    return `https://calendar.google.com/calendar/event?eid=${encodedEventId}&cid=${encodedCalendarId}`;
}

/**
 * Gets the URL for a calendar event
 */
export function getEventUrl(event: calendar_v3.Schema$Event, calendarId?: string): string | null {
    if (event.htmlLink) {
        return event.htmlLink;
    } else if (calendarId && event.id) {
        return generateEventUrl(calendarId, event.id);
    }
    return null;
}

/**
 * Formats a date/time with timezone abbreviation
 */
function formatDateTime(dateTime?: string | null, date?: string | null, timeZone?: string): string {
    if (!dateTime && !date) return "unspecified";
    
    try {
        const dt = dateTime || date;
        if (!dt) return "unspecified";
        
        // If it's a date-only event (all-day), handle it specially
        if (date && !dateTime) {
            // For all-day events, just format the date string directly
            // Date-only strings like "2025-03-15" should be displayed as-is
            const [year, month, day] = date.split('-').map(Number);
            
            // Create a date string without any timezone conversion
            const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 
                              'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
            const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
            
            // Calculate day of week using Zeller's congruence (timezone-independent)
            const q = day;
            const m = month <= 2 ? month + 12 : month;
            const y = month <= 2 ? year - 1 : year;
            const k = y % 100;
            const j = Math.floor(y / 100);
            const h = (q + Math.floor((13 * (m + 1)) / 5) + k + Math.floor(k / 4) + Math.floor(j / 4) - 2 * j) % 7;
            const dayOfWeek = (h + 6) % 7; // Convert to 0=Sunday format
            
            return `${dayNames[dayOfWeek]}, ${monthNames[month - 1]} ${day}, ${year}`;
        }
        
        const parsedDate = new Date(dt);
        if (isNaN(parsedDate.getTime())) return dt;
        
        // For timed events, include timezone
        const options: Intl.DateTimeFormatOptions = {
            weekday: 'short',
            year: 'numeric',
            month: 'short',
            day: 'numeric',
            hour: 'numeric',
            minute: '2-digit',
            timeZoneName: 'short'
        };
        
        if (timeZone) {
            options.timeZone = timeZone;
        }
        
        return parsedDate.toLocaleString('en-US', options);
    } catch (error) {
        return dateTime || date || "unspecified";
    }
}

/**
 * Formats attendees with their response status
 */
function formatAttendees(attendees?: calendar_v3.Schema$EventAttendee[]): string {
    if (!attendees || attendees.length === 0) return "";
    
    const formatted = attendees.map(attendee => {
        const email = attendee.email || "unknown";
        const name = attendee.displayName || email;
        const status = attendee.responseStatus || "unknown";
        
        const statusText = {
            'accepted': 'accepted',
            'declined': 'declined', 
            'tentative': 'tentative',
            'needsAction': 'pending'
        }[status] || 'unknown';
        
        return `${name} (${statusText})`;
    }).join(", ");
    
    return `\nGuests: ${formatted}`;
}

/**
 * Formats a single event with rich details
 */
export function formatEventWithDetails(event: calendar_v3.Schema$Event, calendarId?: string): string {
    const title = event.summary ? `Event: ${event.summary}` : "Untitled Event";
    const eventId = event.id ? `\nEvent ID: ${event.id}` : "";
    const description = event.description ? `\nDescription: ${event.description}` : "";
    const location = event.location ? `\nLocation: ${event.location}` : "";
    const colorId = event.colorId ? `\nColor ID: ${event.colorId}` : "";

    // Format start and end times with timezone
    const startTime = formatDateTime(event.start?.dateTime, event.start?.date, event.start?.timeZone || undefined);
    const endTime = formatDateTime(event.end?.dateTime, event.end?.date, event.end?.timeZone || undefined);
    
    let timeInfo: string;
    if (event.start?.date) {
        // All-day event
        if (event.start.date === event.end?.date) {
            // Single day all-day event
            timeInfo = `\nDate: ${startTime}`;
        } else {
            // Multi-day all-day event - end date is exclusive, so subtract 1 day for display
            if (event.end?.date) {
                // Parse the end date properly without timezone conversion
                const [year, month, day] = event.end.date.split('-').map(Number);
                
                // Subtract 1 day since end is exclusive, handling month/year boundaries
                let adjustedDay = day - 1;
                let adjustedMonth = month;
                let adjustedYear = year;
                
                if (adjustedDay < 1) {
                    adjustedMonth--;
                    if (adjustedMonth < 1) {
                        adjustedMonth = 12;
                        adjustedYear--;
                    }
                    // Get days in the previous month
                    const daysInMonth = new Date(adjustedYear, adjustedMonth, 0).getDate();
                    adjustedDay = daysInMonth;
                }
                
                // Format without using Date object to avoid timezone issues
                const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 
                                  'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
                const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
                
                // Calculate day of week using Zeller's congruence
                const q = adjustedDay;
                const m = adjustedMonth <= 2 ? adjustedMonth + 12 : adjustedMonth;
                const y = adjustedMonth <= 2 ? adjustedYear - 1 : adjustedYear;
                const k = y % 100;
                const j = Math.floor(y / 100);
                const h = (q + Math.floor((13 * (m + 1)) / 5) + k + Math.floor(k / 4) + Math.floor(j / 4) - 2 * j) % 7;
                const dayOfWeek = (h + 6) % 7; // Convert to 0=Sunday format
                
                const adjustedEndTime = `${dayNames[dayOfWeek]}, ${monthNames[adjustedMonth - 1]} ${adjustedDay}, ${adjustedYear}`;
                timeInfo = `\nStart Date: ${startTime}\nEnd Date: ${adjustedEndTime}`;
            } else {
                timeInfo = `\nStart Date: ${startTime}`;
            }
        }
    } else {
        // Timed event
        timeInfo = `\nStart: ${startTime}\nEnd: ${endTime}`;
    }
    
    const attendeeInfo = formatAttendees(event.attendees);
    
    const eventUrl = getEventUrl(event, calendarId);
    const urlInfo = eventUrl ? `\nView: ${eventUrl}` : "";
    
    return `${title}${eventId}${description}${timeInfo}${location}${colorId}${attendeeInfo}${urlInfo}`;
}

/**
 * Formats conflict check results for display
 */
export function formatConflictWarnings(conflicts: ConflictCheckResult): string {
    if (!conflicts.hasConflicts) return "";
    
    let warnings = "";
    
    // Format duplicate warnings
    if (conflicts.duplicates.length > 0) {
        warnings += "\n\n⚠️ POTENTIAL DUPLICATES DETECTED:";
        for (const dup of conflicts.duplicates) {
            warnings += `\n\n━━━ Duplicate Event (${Math.round(dup.event.similarity * 100)}% similar) ━━━`;
            warnings += `\n${dup.suggestion}`;
            
            // Show full event details if available
            if (dup.fullEvent) {
                warnings += `\n\nExisting event details:`;
                warnings += `\n${formatEventWithDetails(dup.fullEvent, dup.calendarId)}`;
            } else {
                // Fallback to basic info
                warnings += `\n• "${dup.event.title}"`;
                if (dup.event.url) {
                    warnings += `\n  View existing event: ${dup.event.url}`;
                }
            }
        }
    }
    
    // Format conflict warnings
    if (conflicts.conflicts.length > 0) {
        warnings += "\n\n⚠️ SCHEDULING CONFLICTS DETECTED:";
        const conflictsByCalendar = conflicts.conflicts.reduce((acc, conflict) => {
            if (!acc[conflict.calendar]) acc[conflict.calendar] = [];
            acc[conflict.calendar].push(conflict);
            return acc;
        }, {} as Record<string, typeof conflicts.conflicts>);
        
        for (const [calendar, calendarConflicts] of Object.entries(conflictsByCalendar)) {
            warnings += `\n\nCalendar: ${calendar}`;
            for (const conflict of calendarConflicts) {
                warnings += `\n\n━━━ Conflicting Event ━━━`;
                if (conflict.overlap) {
                    warnings += `\n⚠️  Overlap: ${conflict.overlap.duration} (${conflict.overlap.percentage}% of your event)`;
                }
                
                // Show full event details if available
                if (conflict.fullEvent) {
                    warnings += `\n\nConflicting event details:`;
                    warnings += `\n${formatEventWithDetails(conflict.fullEvent, calendar)}`;
                } else {
                    // Fallback to basic info
                    warnings += `\n• Conflicts with "${conflict.event.title}"`;
                    if (conflict.event.start && conflict.event.end) {
                        const start = formatDateTime(conflict.event.start);
                        const end = formatDateTime(conflict.event.end);
                        warnings += `\n  Time: ${start} - ${end}`;
                    }
                    if (conflict.event.url) {
                        warnings += `\n  View event: ${conflict.event.url}`;
                    }
                }
            }
        }
    }
    
    return warnings;
}

/**
 * Creates a response with event details and optional conflict warnings
 */
export function createEventResponseWithConflicts(
    event: calendar_v3.Schema$Event,
    calendarId: string,
    conflicts?: ConflictCheckResult,
    actionVerb: string = "created"
): string {
    const eventDetails = formatEventWithDetails(event, calendarId);
    const conflictWarnings = conflicts ? formatConflictWarnings(conflicts) : "";
    
    const successMessage = conflicts?.hasConflicts 
        ? `Event ${actionVerb} with warnings!`
        : `Event ${actionVerb} successfully!`;
    
    return `${successMessage}\n\n${eventDetails}${conflictWarnings}`;
}


```

--------------------------------------------------------------------------------
/src/tests/unit/schemas/schema-compatibility.test.ts:
--------------------------------------------------------------------------------

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

/**
 * Provider-Specific Schema Compatibility Tests
 *
 * These tests ensure that schemas are compatible with different MCP clients
 * by testing what each provider actually receives, not internal implementation.
 *
 * - OpenAI: Receives converted schemas (anyOf flattened to string)
 * - Python MCP: Receives raw schemas (anyOf preserved for native array support)
 * - Claude: Uses raw MCP schemas
 */

// Type for JSON Schema objects (subset of what zod-to-json-schema returns)
interface JSONSchemaObject {
  type?: string;
  properties?: Record<string, any>;
  required?: string[];
  anyOf?: any[];
  [key: string]: any;
}

describe('Provider-Specific Schema Compatibility', () => {
  describe('OpenAI Schema Compatibility', () => {
    // Helper function that mimics OpenAI schema conversion from openai-mcp-integration.test.ts
    const convertMCPSchemaToOpenAI = (mcpSchema: any): any => {
      if (!mcpSchema) {
        return {
          type: 'object',
          properties: {},
          required: []
        };
      }

      return {
        type: 'object',
        properties: enhancePropertiesForOpenAI(mcpSchema.properties || {}),
        required: mcpSchema.required || []
      };
    };

    const enhancePropertiesForOpenAI = (properties: any): any => {
      const enhanced: any = {};

      for (const [key, value] of Object.entries(properties)) {
        const prop = value as any;
        enhanced[key] = { ...prop };

        // Handle anyOf union types (OpenAI doesn't support these well)
        if (prop.anyOf && Array.isArray(prop.anyOf)) {
          const stringType = prop.anyOf.find((t: any) => t.type === 'string');
          if (stringType) {
            enhanced[key] = {
              type: 'string',
              description: `${stringType.description || prop.description || ''} Note: For multiple values, use JSON array string format: '["id1", "id2"]'`.trim()
            };
          } else {
            enhanced[key] = { ...prop.anyOf[0] };
          }
          delete enhanced[key].anyOf;
        }

        // Recursively enhance nested objects
        if (enhanced[key].type === 'object' && enhanced[key].properties) {
          enhanced[key].properties = enhancePropertiesForOpenAI(enhanced[key].properties);
        }

        // Enhance array items if they contain objects
        if (enhanced[key].type === 'array' && enhanced[key].items && enhanced[key].items.properties) {
          enhanced[key].items = {
            ...enhanced[key].items,
            properties: enhancePropertiesForOpenAI(enhanced[key].items.properties)
          };
        }
      }

      return enhanced;
    };

    it('should ensure ALL tools (including list-events) have no problematic features after OpenAI conversion', () => {
      const tools = ToolRegistry.getToolsWithSchemas();
      const problematicFeatures = ['oneOf', 'anyOf', 'allOf', 'not'];
      const issues: string[] = [];

      for (const tool of tools) {
        // Convert to OpenAI format (this is what OpenAI actually sees)
        const openaiSchema = convertMCPSchemaToOpenAI(tool.inputSchema);
        const schemaStr = JSON.stringify(openaiSchema);

        for (const feature of problematicFeatures) {
          if (schemaStr.includes(`"${feature}"`)) {
            issues.push(`Tool "${tool.name}" contains "${feature}" after OpenAI conversion - this will break OpenAI function calling`);
          }
        }
      }

      if (issues.length > 0) {
        throw new Error(`OpenAI schema compatibility issues found:\n${issues.join('\n')}`);
      }
    });

    it('should convert list-events calendarId anyOf to string for OpenAI', () => {
      const tools = ToolRegistry.getToolsWithSchemas();
      const listEventsTool = tools.find(t => t.name === 'list-events');

      expect(listEventsTool).toBeDefined();

      // Convert to OpenAI format
      const openaiSchema = convertMCPSchemaToOpenAI(listEventsTool!.inputSchema);

      // OpenAI should see a simple string type, not anyOf
      expect(openaiSchema.properties.calendarId.type).toBe('string');
      expect(openaiSchema.properties.calendarId.anyOf).toBeUndefined();

      // Description should mention JSON array format
      expect(openaiSchema.properties.calendarId.description).toContain('JSON array string format');
      expect(openaiSchema.properties.calendarId.description).toMatch(/\[".*"\]/);
    });

    it('should ensure all converted schemas are valid objects', () => {
      const tools = ToolRegistry.getToolsWithSchemas();

      for (const tool of tools) {
        const openaiSchema = convertMCPSchemaToOpenAI(tool.inputSchema);

        expect(openaiSchema.type).toBe('object');
        expect(openaiSchema.properties).toBeDefined();
        expect(openaiSchema.required).toBeDefined();
      }
    });
  });

  describe('Python MCP Client Compatibility', () => {
    it('should ensure list-events supports native arrays via anyOf', () => {
      const tools = ToolRegistry.getToolsWithSchemas();
      const listEventsTool = tools.find(t => t.name === 'list-events');

      expect(listEventsTool).toBeDefined();

      // Raw MCP schema should have anyOf for Python clients
      const schema = listEventsTool!.inputSchema as JSONSchemaObject;
      expect(schema.properties).toBeDefined();

      const calendarIdProp = schema.properties!.calendarId;
      expect(calendarIdProp.anyOf).toBeDefined();
      expect(Array.isArray(calendarIdProp.anyOf)).toBe(true);
      expect(calendarIdProp.anyOf.length).toBe(2);

      // Verify it has both string and array options
      const types = calendarIdProp.anyOf.map((t: any) => t.type);
      expect(types).toContain('string');
      expect(types).toContain('array');
    });

    it('should ensure all other tools do NOT use anyOf/oneOf/allOf', () => {
      const tools = ToolRegistry.getToolsWithSchemas();
      const problematicFeatures = ['oneOf', 'anyOf', 'allOf', 'not'];
      const issues: string[] = [];

      for (const tool of tools) {
        // Skip list-events - it's explicitly allowed to use anyOf
        if (tool.name === 'list-events') {
          continue;
        }

        const schemaStr = JSON.stringify(tool.inputSchema);

        for (const feature of problematicFeatures) {
          if (schemaStr.includes(`"${feature}"`)) {
            issues.push(`Tool "${tool.name}" contains problematic feature: ${feature}`);
          }
        }
      }

      if (issues.length > 0) {
        throw new Error(`Raw MCP schema compatibility issues found:\n${issues.join('\n')}`);
      }
    });
  });

  describe('General Schema Structure', () => {
    it('should have tools available', () => {
      const tools = ToolRegistry.getToolsWithSchemas();
      expect(tools).toBeDefined();
      expect(tools.length).toBeGreaterThan(0);
    });

    it('should have proper schema structure for all tools', () => {
      const tools = ToolRegistry.getToolsWithSchemas();
      expect(tools).toBeDefined();
      expect(tools.length).toBeGreaterThan(0);

      for (const tool of tools) {
        const schema = tool.inputSchema as JSONSchemaObject;

        // All schemas should be objects at the top level
        expect(schema.type).toBe('object');
      }
    });

    it('should validate specific known tool schemas exist', () => {
      const tools = ToolRegistry.getToolsWithSchemas();
      const toolSchemas = new Map();
      for (const tool of tools) {
        toolSchemas.set(tool.name, tool.inputSchema);
      }

      // Validate that key tools exist and have the proper basic structure
      const listEventsSchema = toolSchemas.get('list-events') as JSONSchemaObject;
      expect(listEventsSchema).toBeDefined();
      expect(listEventsSchema.type).toBe('object');

      if (listEventsSchema.properties) {
        expect(listEventsSchema.properties.calendarId).toBeDefined();
        expect(listEventsSchema.properties.timeMin).toBeDefined();
        expect(listEventsSchema.properties.timeMax).toBeDefined();
      }

      // Check other important tools exist
      expect(toolSchemas.get('create-event')).toBeDefined();
      expect(toolSchemas.get('update-event')).toBeDefined();
      expect(toolSchemas.get('delete-event')).toBeDefined();
    });

    it('should test that all datetime fields have proper format', () => {
      const tools = ToolRegistry.getToolsWithSchemas();

      const toolsWithDateTimeFields = ['list-events', 'search-events', 'create-event', 'update-event', 'get-freebusy'];

      for (const tool of tools) {
        if (toolsWithDateTimeFields.includes(tool.name)) {
          // These tools should exist and be properly typed
          const schema = tool.inputSchema as JSONSchemaObject;
          expect(schema.type).toBe('object');
        }
      }
    });

    it('should ensure enum fields are properly structured', () => {
      const tools = ToolRegistry.getToolsWithSchemas();

      const toolsWithEnums = ['update-event', 'delete-event'];

      for (const tool of tools) {
        if (toolsWithEnums.includes(tool.name)) {
          // These tools should exist and be properly typed
          const schema = tool.inputSchema as JSONSchemaObject;
          expect(schema.type).toBe('object');
        }
      }
    });

    it('should validate array fields have proper items definition', () => {
      const tools = ToolRegistry.getToolsWithSchemas();

      const toolsWithArrays = ['create-event', 'update-event', 'get-freebusy'];

      for (const tool of tools) {
        if (toolsWithArrays.includes(tool.name)) {
          // These tools should exist and be properly typed
          const schema = tool.inputSchema as JSONSchemaObject;
          expect(schema.type).toBe('object');
        }
      }
    });
  });
});

/**
 * Schema Validation Rules Documentation
 *
 * This test documents the rules that our schemas must follow
 * to be compatible with various MCP clients.
 */
describe('Schema Validation Rules Documentation', () => {
  it('should document provider-specific compatibility requirements', () => {
    const rules = {
      'OpenAI': 'Schemas are converted to remove anyOf/oneOf/allOf. Union types flattened to primary type with usage notes in description.',
      'Python MCP': 'Native array support via anyOf for list-events.calendarId. Accepts both string and array types directly.',
      'Claude/Generic MCP': 'Uses raw schemas. list-events has anyOf for flexibility, but most tools avoid union types for broad compatibility.',
      'Top-level schema': 'All schemas must be type: "object" at root level.',
      'DateTime fields': 'Support both RFC3339 with timezone and timezone-naive formats.',
      'Array fields': 'Must have items schema defined for proper validation.',
      'Enum fields': 'Must include type information alongside enum values.'
    };

    // This test documents the rules - it always passes but serves as documentation
    expect(Object.keys(rules).length).toBeGreaterThan(0);
  });
});

```

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

```typescript
import { OAuth2Client } from 'google-auth-library';
import { TokenManager } from './tokenManager.js';
import http from 'http';
import { URL } from 'url';
import open from 'open';
import { loadCredentials } from './client.js';
import { getAccountMode } from './utils.js';

export class AuthServer {
  private baseOAuth2Client: OAuth2Client; // Used by TokenManager for validation/refresh
  private flowOAuth2Client: OAuth2Client | null = null; // Used specifically for the auth code flow
  private server: http.Server | null = null;
  private tokenManager: TokenManager;
  private portRange: { start: number; end: number };
  private activeConnections: Set<import('net').Socket> = new Set(); // Track active socket connections
  public authCompletedSuccessfully = false; // Flag for standalone script

  constructor(oauth2Client: OAuth2Client) {
    this.baseOAuth2Client = oauth2Client;
    this.tokenManager = new TokenManager(oauth2Client);
    this.portRange = { start: 3500, end: 3505 };
  }

  private createServer(): http.Server {
    const server = http.createServer(async (req, res) => {
      const url = new URL(req.url || '/', `http://${req.headers.host}`);
      
      if (url.pathname === '/') {
        // Root route - show auth link
        const clientForUrl = this.flowOAuth2Client || this.baseOAuth2Client;
        const scopes = ['https://www.googleapis.com/auth/calendar'];
        const authUrl = clientForUrl.generateAuthUrl({
          access_type: 'offline',
          scope: scopes,
          prompt: 'consent'
        });
        
        const accountMode = getAccountMode();
        
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(`
          <h1>Google Calendar Authentication</h1>
          <p><strong>Account Mode:</strong> <code>${accountMode}</code></p>
          <p>You are authenticating for the <strong>${accountMode}</strong> account.</p>
          <a href="${authUrl}">Authenticate with Google</a>
        `);
        
      } else if (url.pathname === '/oauth2callback') {
        // OAuth callback route
        const code = url.searchParams.get('code');
        if (!code) {
          res.writeHead(400, { 'Content-Type': 'text/plain' });
          res.end('Authorization code missing');
          return;
        }
        
        if (!this.flowOAuth2Client) {
          res.writeHead(500, { 'Content-Type': 'text/plain' });
          res.end('Authentication flow not properly initiated.');
          return;
        }
        
        try {
          const { tokens } = await this.flowOAuth2Client.getToken(code);
          await this.tokenManager.saveTokens(tokens);
          this.authCompletedSuccessfully = true;

          const tokenPath = this.tokenManager.getTokenPath();
          const accountMode = this.tokenManager.getAccountMode();
          
          res.writeHead(200, { 'Content-Type': 'text/html' });
          res.end(`
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Authentication Successful</title>
                <style>
                    body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f4f4f4; margin: 0; }
                    .container { text-align: center; padding: 2em; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
                    h1 { color: #4CAF50; }
                    p { color: #333; margin-bottom: 0.5em; }
                    code { background-color: #eee; padding: 0.2em 0.4em; border-radius: 3px; font-size: 0.9em; }
                    .account-mode { background-color: #e3f2fd; padding: 1em; border-radius: 5px; margin: 1em 0; }
                </style>
            </head>
            <body>
                <div class="container">
                    <h1>Authentication Successful!</h1>
                    <div class="account-mode">
                        <p><strong>Account Mode:</strong> <code>${accountMode}</code></p>
                        <p>Your authentication tokens have been saved for the <strong>${accountMode}</strong> account.</p>
                    </div>
                    <p>Tokens saved to:</p>
                    <p><code>${tokenPath}</code></p>
                    <p>You can now close this browser window.</p>
                </div>
            </body>
            </html>
          `);
        } catch (error: unknown) {
          this.authCompletedSuccessfully = false;
          const message = error instanceof Error ? error.message : 'Unknown error';
          process.stderr.write(`✗ Token save failed: ${message}\n`);

          res.writeHead(500, { 'Content-Type': 'text/html' });
          res.end(`
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Authentication Failed</title>
                <style>
                    body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f4f4f4; margin: 0; }
                    .container { text-align: center; padding: 2em; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
                    h1 { color: #F44336; }
                    p { color: #333; }
                </style>
            </head>
            <body>
                <div class="container">
                    <h1>Authentication Failed</h1>
                    <p>An error occurred during authentication:</p>
                    <p><code>${message}</code></p>
                    <p>Please try again or check the server logs.</p>
                </div>
            </body>
            </html>
          `);
        }
      } else {
        // 404 for other routes
        res.writeHead(404, { 'Content-Type': 'text/plain' });
        res.end('Not Found');
      }
    });

    // Track connections at server level
    server.on('connection', (socket) => {
      this.activeConnections.add(socket);
      socket.on('close', () => {
        this.activeConnections.delete(socket);
      });
    });
    
    return server;
  }

  async start(openBrowser = true): Promise<boolean> {
    // Add timeout wrapper to prevent hanging
    return Promise.race([
      this.startWithTimeout(openBrowser),
      new Promise<boolean>((_, reject) => {
        setTimeout(() => reject(new Error('Auth server start timed out after 10 seconds')), 10000);
      })
    ]).catch(() => false); // Return false on timeout instead of throwing
  }

  private async startWithTimeout(openBrowser = true): Promise<boolean> {
    if (await this.tokenManager.validateTokens()) {
      this.authCompletedSuccessfully = true;
      return true;
    }
    
    // Try to start the server and get the port
    const port = await this.startServerOnAvailablePort();
    if (port === null) {
      process.stderr.write(`Could not start auth server on available port. Please check port availability (${this.portRange.start}-${this.portRange.end}) and try again.\n`);

      this.authCompletedSuccessfully = false;
      return false;
    }

    // Successfully started server on `port`. Now create the flow-specific OAuth client.
    try {
      const { client_id, client_secret } = await loadCredentials();
      this.flowOAuth2Client = new OAuth2Client(
        client_id,
        client_secret,
        `http://localhost:${port}/oauth2callback`
      );
    } catch (error) {
        // Could not load credentials, cannot proceed with auth flow
        this.authCompletedSuccessfully = false;
        await this.stop(); // Stop the server we just started
        return false;
    }

    // Generate Auth URL using the newly created flow client
    const authorizeUrl = this.flowOAuth2Client.generateAuthUrl({
      access_type: 'offline',
      scope: ['https://www.googleapis.com/auth/calendar'],
      prompt: 'consent'
    });
    
    // Always show the URL in console for easy access
    process.stderr.write(`\n🔗 Authentication URL: ${authorizeUrl}\n\n`);
    process.stderr.write(`Or visit: http://localhost:${port}\n\n`);
    
    if (openBrowser) {
      try {
        await open(authorizeUrl);
        process.stderr.write(`Browser opened automatically. If it didn't open, use the URL above.\n`);
      } catch (error) {
        process.stderr.write(`Could not open browser automatically. Please use the URL above.\n`);
      }
    } else {
      process.stderr.write(`Please visit the URL above to complete authentication.\n`);
    }

    return true; // Auth flow initiated
  }

  private async startServerOnAvailablePort(): Promise<number | null> {
    for (let port = this.portRange.start; port <= this.portRange.end; port++) {
      try {
        await new Promise<void>((resolve, reject) => {
          const testServer = this.createServer();
          testServer.listen(port, () => {
            this.server = testServer; // Assign to class property *only* if successful
            resolve();
          });
          testServer.on('error', (err: NodeJS.ErrnoException) => {
            if (err.code === 'EADDRINUSE') {
              // Port is in use, close the test server and reject
              testServer.close(() => reject(err)); 
            } else {
              // Other error, reject
              reject(err);
            }
          });
        });
        return port; // Port successfully bound
      } catch (error: unknown) {
        // Check if it's EADDRINUSE, otherwise rethrow or handle
        if (!(error instanceof Error && 'code' in error && error.code === 'EADDRINUSE')) {
            // An unexpected error occurred during server start
            return null;
        }
        // EADDRINUSE occurred, loop continues
      }
    }
    return null; // No port found
  }

  public getRunningPort(): number | null {
    if (this.server) {
      const address = this.server.address();
      if (typeof address === 'object' && address !== null) {
        return address.port;
      }
    }
    return null;
  }

  async stop(): Promise<void> {
    return new Promise((resolve, reject) => {
      if (this.server) {
        // Force close all active connections
        for (const connection of this.activeConnections) {
          connection.destroy();
        }
        this.activeConnections.clear();
        
        // Add a timeout to force close if server doesn't close gracefully
        const timeout = setTimeout(() => {
          process.stderr.write('Server close timeout, forcing exit...\n');
          this.server = null;
          resolve();
        }, 2000); // 2 second timeout
        
        this.server.close((err) => {
          clearTimeout(timeout);
          if (err) {
            reject(err);
          } else {
            this.server = null;
            resolve();
          }
        });
      } else {
        resolve();
      }
    });
  }
} 
```
Page 2/5FirstPrevNextLast