#
tokens: 49288/50000 25/127 files (page 2/3)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 3. Use http://codebase.md/aaronsb/google-workspace-mcp?page={x} to view the full context.

# Directory Structure

```
├── .dockerignore
├── .eslintrc.json
├── .github
│   ├── config.yml
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── pull_request_template.md
│   └── workflows
│       ├── ci.yml
│       └── docker-publish.yml
├── .gitignore
├── ARCHITECTURE.md
├── cline_docs
│   ├── activeContext.md
│   ├── productContext.md
│   ├── progress.md
│   ├── systemPatterns.md
│   └── techContext.md
├── CODE_OF_CONDUCT.md
├── config
│   ├── accounts.example.json
│   ├── credentials
│   │   └── README.md
│   └── gauth.example.json
├── CONTRIBUTING.md
├── docker-entrypoint.sh
├── Dockerfile
├── Dockerfile.local
├── docs
│   ├── API.md
│   ├── assets
│   │   └── robot-assistant.png
│   ├── automatic-oauth-flow.md
│   ├── ERRORS.md
│   ├── EXAMPLES.md
│   └── TOOL_DISCOVERY.md
├── jest.config.cjs
├── jest.setup.cjs
├── LICENSE
├── llms-install.md
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   ├── build-local.sh
│   └── local-entrypoint.sh
├── SECURITY.md
├── smithery.yaml
├── src
│   ├── __fixtures__
│   │   └── accounts.ts
│   ├── __helpers__
│   │   ├── package.json
│   │   └── testSetup.ts
│   ├── __mocks__
│   │   ├── @modelcontextprotocol
│   │   │   ├── sdk
│   │   │   │   ├── server
│   │   │   │   │   ├── index.js
│   │   │   │   │   └── stdio.js
│   │   │   │   └── types.ts
│   │   │   └── sdk.ts
│   │   ├── googleapis.ts
│   │   └── logger.ts
│   ├── __tests__
│   │   └── modules
│   │       ├── accounts
│   │       │   ├── manager.test.ts
│   │       │   └── token.test.ts
│   │       ├── attachments
│   │       │   └── index.test.ts
│   │       ├── calendar
│   │       │   └── service.test.ts
│   │       └── gmail
│   │           └── service.test.ts
│   ├── api
│   │   ├── handler.ts
│   │   ├── request.ts
│   │   └── validators
│   │       ├── endpoint.ts
│   │       └── parameter.ts
│   ├── index.ts
│   ├── modules
│   │   ├── accounts
│   │   │   ├── callback-server.ts
│   │   │   ├── index.ts
│   │   │   ├── manager.ts
│   │   │   ├── oauth.ts
│   │   │   ├── token.ts
│   │   │   └── types.ts
│   │   ├── attachments
│   │   │   ├── cleanup-service.ts
│   │   │   ├── index-service.ts
│   │   │   ├── response-transformer.ts
│   │   │   ├── service.ts
│   │   │   ├── transformer.ts
│   │   │   └── types.ts
│   │   ├── calendar
│   │   │   ├── __tests__
│   │   │   │   └── scopes.test.ts
│   │   │   ├── index.ts
│   │   │   ├── scopes.ts
│   │   │   ├── service.ts
│   │   │   └── types.ts
│   │   ├── contacts
│   │   │   ├── index.ts
│   │   │   ├── scopes.ts
│   │   │   └── types.ts
│   │   ├── drive
│   │   │   ├── __tests__
│   │   │   │   ├── scopes.test.ts
│   │   │   │   └── service.test.ts
│   │   │   ├── index.ts
│   │   │   ├── scopes.ts
│   │   │   ├── service.ts
│   │   │   └── types.ts
│   │   ├── gmail
│   │   │   ├── __tests__
│   │   │   │   ├── label.test.ts
│   │   │   │   └── scopes.test.ts
│   │   │   ├── constants.ts
│   │   │   ├── index.ts
│   │   │   ├── scopes.ts
│   │   │   ├── service.ts
│   │   │   ├── services
│   │   │   │   ├── attachment.ts
│   │   │   │   ├── base.ts
│   │   │   │   ├── draft.ts
│   │   │   │   ├── email.ts
│   │   │   │   ├── label.ts
│   │   │   │   ├── search.ts
│   │   │   │   └── settings.ts
│   │   │   └── types.ts
│   │   └── tools
│   │       ├── __tests__
│   │       │   ├── registry.test.ts
│   │       │   └── scope-registry.test.ts
│   │       ├── registry.ts
│   │       └── scope-registry.ts
│   ├── oauth
│   │   └── client.ts
│   ├── scripts
│   │   ├── health-check.ts
│   │   ├── setup-environment.ts
│   │   └── setup-google-env.ts
│   ├── services
│   │   ├── base
│   │   │   └── BaseGoogleService.ts
│   │   ├── calendar
│   │   │   └── index.ts
│   │   ├── contacts
│   │   │   └── index.ts
│   │   ├── drive
│   │   │   └── index.ts
│   │   └── gmail
│   │       └── index.ts
│   ├── tools
│   │   ├── account-handlers.ts
│   │   ├── calendar-handlers.ts
│   │   ├── contacts-handlers.ts
│   │   ├── definitions.ts
│   │   ├── drive-handlers.ts
│   │   ├── gmail-handlers.ts
│   │   ├── server.ts
│   │   ├── type-guards.ts
│   │   └── types.ts
│   ├── types
│   │   └── modelcontextprotocol__sdk.d.ts
│   ├── types.ts
│   └── utils
│       ├── account.ts
│       ├── logger.ts
│       ├── service-initializer.ts
│       ├── token.ts
│       └── workspace.ts
├── TODO.md
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/src/__tests__/modules/attachments/index.test.ts:
--------------------------------------------------------------------------------

```typescript
import { AttachmentIndexService } from '../../../modules/attachments/index-service.js';
import { AttachmentCleanupService } from '../../../modules/attachments/cleanup-service.js';
import { AttachmentResponseTransformer } from '../../../modules/attachments/response-transformer.js';

describe('Attachment System', () => {
  let indexService: AttachmentIndexService;
  let cleanupService: AttachmentCleanupService;
  let responseTransformer: AttachmentResponseTransformer;

  beforeEach(() => {
    // Reset singleton instance for test isolation
    // @ts-expect-error - Accessing private static for testing
    AttachmentIndexService.instance = undefined;
    indexService = AttachmentIndexService.getInstance();
    cleanupService = new AttachmentCleanupService(indexService);
    responseTransformer = new AttachmentResponseTransformer(indexService);
  });

  afterEach(() => {
    cleanupService.stop();
    jest.clearAllTimers();
  });

  describe('AttachmentIndexService', () => {
    it('should store and retrieve attachment metadata', () => {
      const messageId = 'msg123';
      const attachment = {
        id: 'att123',
        name: 'test.pdf',
        mimeType: 'application/pdf',
        size: 1024
      };

      indexService.addAttachment(messageId, attachment);
      const metadata = indexService.getMetadata(messageId, 'test.pdf');

      expect(metadata).toBeDefined();
      expect(metadata?.originalId).toBe('att123');
      expect(metadata?.filename).toBe('test.pdf');
      expect(metadata?.mimeType).toBe('application/pdf');
      expect(metadata?.size).toBe(1024);
    });

    it('should handle size limits', () => {
      // Add max entries + 1
      for (let i = 0; i < 257; i++) {
        indexService.addAttachment(`msg${i}`, {
          id: `att${i}`,
          name: 'test.pdf',
          mimeType: 'application/pdf',
          size: 1024
        });
      }

      // Size should be maintained at or below max
      expect(indexService.size).toBeLessThanOrEqual(256);
    });

    it('should handle expiry', () => {
      const messageId = 'msg123';
      const attachment = {
        id: 'att123',
        name: 'test.pdf',
        mimeType: 'application/pdf',
        size: 1024
      };

      // Add attachment
      indexService.addAttachment(messageId, attachment);
      
      // Mock time passing
      jest.useFakeTimers();
      jest.advanceTimersByTime(3600000 + 1000); // 1 hour + 1 second

      // Attempt to retrieve expired attachment
      const metadata = indexService.getMetadata(messageId, 'test.pdf');
      expect(metadata).toBeUndefined();

      jest.useRealTimers();
    });
  });

  describe('AttachmentCleanupService', () => {
    it('should start and stop cleanup service', () => {
      cleanupService.start();
      expect(cleanupService.getCurrentInterval()).toBe(300000); // Base interval
      cleanupService.stop();
      expect(cleanupService.getCurrentInterval()).toBe(300000);
    });
  });

  describe('AttachmentResponseTransformer', () => {
    it('should transform attachments to simplified format', () => {
      const messageId = 'msg123';
      const fullResponse = {
        id: messageId,
        attachments: [{
          id: 'att123',
          name: 'test.pdf',
          mimeType: 'application/pdf',
          size: 1024
        }]
      };

      // Store attachment metadata
      indexService.addAttachment(messageId, fullResponse.attachments[0]);

      // Transform response
      const simplified = responseTransformer.transformResponse(fullResponse);

      expect(simplified).toEqual({
        id: messageId,
        attachments: [{
          name: 'test.pdf'
        }]
      });
    });

    it('should handle responses without attachments', () => {
      const response = {
        id: 'msg123',
        subject: 'Test'
      };

      const simplified = responseTransformer.transformResponse(response);
      expect(simplified).toEqual(response);
    });
  });

  describe('Integration', () => {
    it('should maintain attachment metadata through transform cycle', () => {
      const messageId = 'msg123';
      const originalAttachment = {
        id: 'att123',
        name: 'test.pdf',
        mimeType: 'application/pdf',
        size: 1024
      };

      // Add attachment and transform response
      indexService.addAttachment(messageId, originalAttachment);
      const transformed = responseTransformer.transformResponse({
        id: messageId,
        attachments: [originalAttachment]
      });

      // Verify simplified format
      expect(transformed.attachments?.[0]).toEqual({
        name: 'test.pdf'
      });

      // Verify original metadata is preserved
      const metadata = indexService.getMetadata(messageId, 'test.pdf');
      expect(metadata).toEqual({
        messageId,
        filename: 'test.pdf',
        originalId: 'att123',
        mimeType: 'application/pdf',
        size: 1024,
        timestamp: expect.any(Number)
      });
    });

    it('should notify activity without error', () => {
      cleanupService.start();
      cleanupService.notifyActivity(); // Should not throw
      cleanupService.stop();
    });
  });
});

```

--------------------------------------------------------------------------------
/src/api/validators/parameter.ts:
--------------------------------------------------------------------------------

```typescript
import { GoogleApiRequestParams, GoogleApiError } from '../../types.js';

interface ParameterConfig {
  required: string[];
  types: Record<string, string>;
}

export class ParameterValidator {
  // Registry of endpoint-specific parameter configurations
  private readonly parameterRegistry: Record<string, ParameterConfig> = {
    'gmail.users.messages.attachments.get': {
      required: ['userId', 'messageId', 'filename'],
      types: {
        userId: 'string',
        messageId: 'string',
        filename: 'string'
      }
    },
    'gmail.users.messages.attachments.upload': {
      required: ['userId', 'messageId', 'filename', 'content'],
      types: {
        userId: 'string',
        messageId: 'string',
        filename: 'string',
        content: 'string'
      }
    },
    'gmail.users.messages.attachments.delete': {
      required: ['userId', 'messageId', 'filename'],
      types: {
        userId: 'string',
        messageId: 'string',
        filename: 'string'
      }
    },
    'calendar.events.attachments.get': {
      required: ['calendarId', 'eventId', 'filename'],
      types: {
        calendarId: 'string',
        eventId: 'string',
        filename: 'string'
      }
    },
    'calendar.events.attachments.upload': {
      required: ['calendarId', 'eventId', 'filename', 'content'],
      types: {
        calendarId: 'string',
        eventId: 'string',
        filename: 'string',
        content: 'string'
      }
    },
    'calendar.events.attachments.delete': {
      required: ['calendarId', 'eventId', 'filename'],
      types: {
        calendarId: 'string',
        eventId: 'string',
        filename: 'string'
      }
    },
    'gmail.users.messages.list': {
      required: ['userId'],
      types: {
        userId: 'string',
        maxResults: 'number',
        pageToken: 'string',
        q: 'string',
        labelIds: 'array'
      }
    },
    'gmail.users.messages.get': {
      required: ['userId', 'id'],
      types: {
        userId: 'string',
        id: 'string',
        format: 'string'
      }
    },
    'gmail.users.messages.send': {
      required: ['userId', 'message'],
      types: {
        userId: 'string',
        message: 'object'
      }
    },
    'drive.files.list': {
      required: [],
      types: {
        pageSize: 'number',
        pageToken: 'string',
        q: 'string',
        spaces: 'string',
        fields: 'string'
      }
    },
    'drive.files.get': {
      required: ['fileId'],
      types: {
        fileId: 'string',
        fields: 'string',
        acknowledgeAbuse: 'boolean'
      }
    }
  };

  async validate(params: GoogleApiRequestParams): Promise<void> {
    const { api_endpoint, params: methodParams = {} } = params;

    // Get parameter configuration for this endpoint
    const config = this.parameterRegistry[api_endpoint];
    if (!config) {
      // If no specific config exists, only validate the base required params
      this.validateBaseParams(params);
      return;
    }

    // Validate required parameters
    this.validateRequiredParams(api_endpoint, methodParams, config.required);

    // Validate parameter types
    this.validateParamTypes(api_endpoint, methodParams, config.types);
  }

  private validateBaseParams(params: GoogleApiRequestParams): void {
    const requiredBaseParams = ['email', 'api_endpoint', 'method', 'required_scopes'];
    const missingParams = requiredBaseParams.filter(param => !(param in params));

    if (missingParams.length > 0) {
      throw new GoogleApiError(
        'Missing required parameters',
        'MISSING_REQUIRED_PARAMS',
        `The following parameters are required: ${missingParams.join(', ')}`
      );
    }
  }

  private validateRequiredParams(
    endpoint: string,
    params: Record<string, any>,
    required: string[]
  ): void {
    const missingParams = required.filter(param => !(param in params));

    if (missingParams.length > 0) {
      throw new GoogleApiError(
        'Missing required parameters',
        'MISSING_REQUIRED_PARAMS',
        `The following parameters are required for ${endpoint}: ${missingParams.join(', ')}`
      );
    }
  }

  private validateParamTypes(
    endpoint: string,
    params: Record<string, any>,
    types: Record<string, string>
  ): void {
    for (const [param, value] of Object.entries(params)) {
      const expectedType = types[param];
      if (!expectedType) {
        // Skip validation for parameters not in the type registry
        continue;
      }

      const actualType = this.getType(value);
      if (actualType !== expectedType) {
        throw new GoogleApiError(
          'Invalid parameter type',
          'INVALID_PARAM_TYPE',
          `Parameter "${param}" for ${endpoint} must be of type ${expectedType}, got ${actualType}`
        );
      }
    }
  }

  private getType(value: any): string {
    if (Array.isArray(value)) return 'array';
    if (value === null) return 'null';
    if (typeof value === 'object') return 'object';
    return typeof value;
  }

  getRequiredParams(endpoint: string): string[] {
    return this.parameterRegistry[endpoint]?.required || [];
  }

  getParamTypes(endpoint: string): Record<string, string> {
    return this.parameterRegistry[endpoint]?.types || {};
  }
}

```

--------------------------------------------------------------------------------
/src/oauth/client.ts:
--------------------------------------------------------------------------------

```typescript
import fs from 'fs/promises';
import path from 'path';
import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
import { OAuthConfig, TokenData, GoogleApiError } from '../types.js';

export class GoogleOAuthClient {
  private client?: OAuth2Client;
  private config?: OAuthConfig;
  private initializationPromise?: Promise<void>;

  async ensureInitialized(): Promise<void> {
    if (!this.initializationPromise) {
      this.initializationPromise = this.loadConfig().catch(error => {
        // Clear the promise so we can retry initialization
        this.initializationPromise = undefined;
        throw error;
      });
    }
    await this.initializationPromise;
  }

  private async loadConfig(): Promise<void> {
    // First try environment variables
    const clientId = process.env.GOOGLE_CLIENT_ID;
    const clientSecret = process.env.GOOGLE_CLIENT_SECRET;

    if (clientId && clientSecret) {
      this.config = {
        client_id: clientId,
        client_secret: clientSecret,
        auth_uri: 'https://accounts.google.com/o/oauth2/v2/auth',
        token_uri: 'https://oauth2.googleapis.com/token'
      };
    } else {
      // Fall back to config file if environment variables are not set
      try {
        const configPath = process.env.GAUTH_FILE || path.resolve('config', 'gauth.json');
        const data = await fs.readFile(configPath, 'utf-8');
        this.config = JSON.parse(data) as OAuthConfig;
      } catch (error) {
        if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
          throw new GoogleApiError(
            'OAuth credentials not found',
            'CONFIG_NOT_FOUND',
            'Please provide GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables or ensure config/gauth.json exists'
          );
        }
        throw new GoogleApiError(
          'Failed to load OAuth configuration',
          'OAUTH_CONFIG_ERROR',
          'Please check your environment variables or ensure gauth.json is valid'
        );
      }
    }

    if (!this.config) {
      throw new GoogleApiError(
        'OAuth configuration not available',
        'CONFIG_NOT_FOUND',
        'Please provide OAuth credentials through environment variables or config file'
      );
    }

    this.client = new google.auth.OAuth2(
      this.config.client_id,
      this.config.client_secret,
      'urn:ietf:wg:oauth:2.0:oob'  // Use device code flow
    );
  }

  async generateAuthUrl(scopes: string[]): Promise<string> {
    await this.ensureInitialized();
    if (!this.config || !this.client) {
      throw new GoogleApiError(
        'OAuth client not initialized',
        'CLIENT_NOT_INITIALIZED',
        'Please ensure the OAuth configuration is loaded'
      );
    }

    return this.client.generateAuthUrl({
      access_type: 'offline',
      scope: scopes,
      prompt: 'consent'  // Force consent screen to ensure we get refresh token
    });
  }

  async getTokenFromCode(code: string): Promise<TokenData> {
    await this.ensureInitialized();
    if (!this.client) {
      throw new GoogleApiError(
        'OAuth client not initialized',
        'CLIENT_NOT_INITIALIZED',
        'Please ensure the OAuth configuration is loaded'
      );
    }

    try {
      const { tokens } = await this.client.getToken(code);
      
      if (!tokens.refresh_token) {
        throw new GoogleApiError(
          'No refresh token received',
          'NO_REFRESH_TOKEN',
          'Please ensure you have included offline access in your scopes'
        );
      }

      return {
        access_token: tokens.access_token!,
        refresh_token: tokens.refresh_token,
        scope: tokens.scope!,
        token_type: tokens.token_type!,
        expiry_date: tokens.expiry_date!,
        last_refresh: Date.now()
      };
    } catch (error) {
      if (error instanceof GoogleApiError) {
        throw error;
      }
      throw new GoogleApiError(
        'Failed to get token from code',
        'TOKEN_EXCHANGE_ERROR',
        'The authorization code may be invalid or expired'
      );
    }
  }

  async refreshToken(refreshToken: string): Promise<TokenData> {
    await this.ensureInitialized();
    if (!this.client) {
      throw new GoogleApiError(
        'OAuth client not initialized',
        'CLIENT_NOT_INITIALIZED',
        'Please ensure the OAuth configuration is loaded'
      );
    }

    try {
      this.client.setCredentials({
        refresh_token: refreshToken
      });

      const { credentials } = await this.client.refreshAccessToken();
      
      return {
        access_token: credentials.access_token!,
        refresh_token: refreshToken, // Keep existing refresh token
        scope: credentials.scope!,
        token_type: credentials.token_type!,
        expiry_date: credentials.expiry_date!,
        last_refresh: Date.now()
      };
    } catch (error) {
      throw new GoogleApiError(
        'Failed to refresh token',
        'TOKEN_REFRESH_ERROR',
        'The refresh token may be invalid or revoked'
      );
    }
  }

  async validateToken(token: TokenData): Promise<boolean> {
    await this.ensureInitialized();
    if (!this.client) {
      throw new GoogleApiError(
        'OAuth client not initialized',
        'CLIENT_NOT_INITIALIZED',
        'Please ensure the OAuth configuration is loaded'
      );
    }

    try {
      this.client.setCredentials({
        access_token: token.access_token,
        refresh_token: token.refresh_token
      });

      await this.client.getTokenInfo(token.access_token);
      return true;
    } catch (error) {
      return false;
    }
  }

  async getAuthClient(): Promise<OAuth2Client> {
    await this.ensureInitialized();
    if (!this.client) {
      throw new GoogleApiError(
        'OAuth client not initialized',
        'CLIENT_NOT_INITIALIZED',
        'Please ensure the OAuth configuration is loaded'
      );
    }
    return this.client;
  }
}

```

--------------------------------------------------------------------------------
/src/tools/account-handlers.ts:
--------------------------------------------------------------------------------

```typescript
import { getAccountManager } from '../modules/accounts/index.js';
import { McpToolResponse, BaseToolArguments } from './types.js';

/**
 * Lists all configured Google Workspace accounts and their authentication status
 * @returns List of accounts with their configuration and auth status
 * @throws {McpError} If account manager fails to retrieve accounts
 */
export async function handleListWorkspaceAccounts(): Promise<McpToolResponse> {
  const accounts = await getAccountManager().listAccounts();
  
  // Filter out sensitive token data before returning to AI
  const sanitizedAccounts = accounts.map(account => ({
    ...account,
    auth_status: account.auth_status ? {
      valid: account.auth_status.valid,
      status: account.auth_status.status,
      reason: account.auth_status.reason,
      authUrl: account.auth_status.authUrl
    } : undefined
  }));

  return {
    content: [{
      type: 'text',
      text: JSON.stringify(sanitizedAccounts, null, 2)
    }]
  };
}

export interface AuthenticateAccountArgs extends BaseToolArguments {
  category?: string;
  description?: string;
  auth_code?: string;
  auto_complete?: boolean;
}

/**
 * Authenticates a Google Workspace account through OAuth2
 * @param args.email - Email address to authenticate
 * @param args.category - Optional account category (e.g., 'work', 'personal')
 * @param args.description - Optional account description
 * @param args.auth_code - OAuth2 authorization code (optional for manual flow)
 * @param args.auto_complete - Whether to automatically complete auth (default: true)
 * @returns Auth URL and instructions for completing authentication
 * @throws {McpError} If validation fails or OAuth flow errors
 */
export async function handleAuthenticateWorkspaceAccount(args: AuthenticateAccountArgs): Promise<McpToolResponse> {
  const accountManager = getAccountManager();

  // Validate/create account
  await accountManager.validateAccount(args.email, args.category, args.description);

  // If auth code is provided (manual fallback), complete the OAuth flow
  if (args.auth_code) {
    const tokenData = await accountManager.getTokenFromCode(args.auth_code);
    await accountManager.saveToken(args.email, tokenData);
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          status: 'success',
          message: 'Authentication successful! Token saved. Please retry your request.'
        }, null, 2)
      }]
    };
  }

  // Generate OAuth URL and track which account is being authenticated
  const authUrl = await accountManager.startAuthentication(args.email);
  
  // Check if we should use automatic completion (default: true)
  const useAutoComplete = args.auto_complete !== false;
  
  return {
    content: [{
      type: 'text',
      text: JSON.stringify({
        status: 'auth_required',
        auth_url: authUrl,
        message: 'Please complete Google OAuth authentication:',
        instructions: useAutoComplete ? [
          '1. Click the authorization URL to open Google sign-in in your browser',
          '2. Sign in with your Google account and allow the requested permissions',
          '3. Authentication will complete automatically - you can start using the account immediately'
        ].join('\n') : [
          '1. Click the authorization URL below to open Google sign-in in your browser',
          '2. Sign in with your Google account and allow the requested permissions',
          '3. After authorization, you will see a success page with your authorization code',
          '4. Copy the authorization code from the success page',
          '5. Call this tool again with the auth_code parameter: authenticate_workspace_account with auth_code="your_code_here"'
        ].join('\n'),
        note: useAutoComplete 
          ? 'The callback server will automatically complete authentication in the background.'
          : 'The callback server is running on localhost:8080 and will display your authorization code for easy copying.',
        auto_complete_enabled: useAutoComplete
      }, null, 2)
    }]
  };
}

/**
 * Completes OAuth authentication automatically by waiting for callback
 * @param args.email - Email address to authenticate
 * @returns Success message when authentication completes
 * @throws {McpError} If authentication times out or fails
 */
export async function handleCompleteWorkspaceAuth(args: BaseToolArguments): Promise<McpToolResponse> {
  const accountManager = getAccountManager();
  
  try {
    // Wait for the authorization code from the callback server
    const code = await accountManager.waitForAuthorizationCode();
    
    // Exchange code for tokens
    const tokenData = await accountManager.getTokenFromCode(code);
    await accountManager.saveToken(args.email, tokenData);
    
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          status: 'success',
          message: 'Authentication completed automatically! Your account is now ready to use.'
        }, null, 2)
      }]
    };
  } catch (error) {
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          status: 'error',
          message: 'Authentication timeout or error. Please use the manual authentication flow.',
          error: error instanceof Error ? error.message : 'Unknown error'
        }, null, 2)
      }]
    };
  }
}

/**
 * Removes a Google Workspace account and its associated authentication tokens
 * @param args.email - Email address of the account to remove
 * @returns Success message if account removed
 * @throws {McpError} If account removal fails
 */
export async function handleRemoveWorkspaceAccount(args: BaseToolArguments): Promise<McpToolResponse> {
  await getAccountManager().removeAccount(args.email);
  
  return {
    content: [{
      type: 'text',
      text: JSON.stringify({
        status: 'success',
        message: `Successfully removed account ${args.email} and deleted associated tokens`
      }, null, 2)
    }]
  };
}

```

--------------------------------------------------------------------------------
/src/modules/gmail/types.ts:
--------------------------------------------------------------------------------

```typescript
export interface BaseGmailAttachment {
  id: string;          // Gmail attachment ID
  name: string;        // Filename
  mimeType: string;    // MIME type
  size: number;        // Size in bytes
}

export interface IncomingGmailAttachment extends BaseGmailAttachment {
  content?: string;    // Base64 content when retrieved
  path?: string;       // Local filesystem path when downloaded
}

export interface OutgoingGmailAttachment extends BaseGmailAttachment {
  content: string;     // Base64 content required when sending
}

export type GmailAttachment = IncomingGmailAttachment | OutgoingGmailAttachment;

export interface Label {
  id: string;
  name: string;
  messageListVisibility?: 'show' | 'hide';
  labelListVisibility?: 'labelShow' | 'labelHide' | 'labelShowIfUnread';
  type?: 'system' | 'user';
  color?: {
    textColor: string;
    backgroundColor: string;
  };
}

export interface DraftResponse {
  id: string;
  message: {
    id: string;
    threadId: string;
    labelIds: string[];
  };
  updated: string;
}

export interface GetDraftsResponse {
  drafts: DraftResponse[];
  nextPageToken?: string;
  resultSizeEstimate?: number;
}

export interface DraftEmailParams {
  to: string[];
  subject: string;
  body: string;
  cc?: string[];
  bcc?: string[];
  attachments?: {
    driveFileId?: string;
    content?: string;
    name: string;
    mimeType: string;
    size?: number;
  }[];
}

export interface GetDraftsParams {
  email: string;
  maxResults?: number;
  pageToken?: string;
}

export interface SendDraftParams {
  email: string;
  draftId: string;
}

export interface GetLabelsResponse {
  labels: Label[];
  nextPageToken?: string;
}

export interface LabelFilter {
  id: string;
  labelId: string;
  criteria: LabelFilterCriteria;
  actions: LabelFilterActions;
}

export interface GetLabelFiltersResponse {
  filters: LabelFilter[];
}

export interface CreateLabelParams {
  email: string;
  name: string;
  messageListVisibility?: 'show' | 'hide';
  labelListVisibility?: 'labelShow' | 'labelHide' | 'labelShowIfUnread';
  color?: {
    textColor: string;
    backgroundColor: string;
  };
}

export interface UpdateLabelParams {
  email: string;
  labelId: string;
  name?: string;
  messageListVisibility?: 'show' | 'hide';
  labelListVisibility?: 'labelShow' | 'labelHide' | 'labelShowIfUnread';
  color?: {
    textColor: string;
    backgroundColor: string;
  };
}

export interface DeleteLabelParams {
  email: string;
  labelId: string;
}

export interface GetLabelsParams {
  email: string;
}

export interface ModifyMessageLabelsParams {
  email: string;
  messageId: string;
  addLabelIds: string[];
  removeLabelIds: string[];
}

export interface LabelFilterCriteria {
  from?: string[];
  to?: string[];
  subject?: string;
  hasWords?: string[];
  doesNotHaveWords?: string[];
  hasAttachment?: boolean;
  size?: {
    operator: 'larger' | 'smaller';
    size: number;
  };
}

export interface LabelFilterActions {
  addLabel?: boolean;
  markImportant?: boolean;
  markRead?: boolean;
  archive?: boolean;
}

export interface CreateLabelFilterParams {
  email: string;
  labelId: string;
  criteria: LabelFilterCriteria;
  actions: LabelFilterActions;
}

export interface GetLabelFiltersParams {
  email: string;
  labelId?: string;
}

export interface UpdateLabelFilterParams {
  email: string;
  filterId: string;
  labelId: string;
  criteria: LabelFilterCriteria;
  actions: LabelFilterActions;
}

export interface DeleteLabelFilterParams {
  email: string;
  filterId: string;
}

export interface GetGmailSettingsParams {
  email: string;
}

export interface GetGmailSettingsResponse {
  profile: {
    emailAddress: string;
    messagesTotal: number;
    threadsTotal: number;
    historyId: string;
  };
  settings: {
    language?: {
      displayLanguage: string;
    };
    autoForwarding?: {
      enabled: boolean;
      emailAddress?: string;
    };
    imap?: {
      enabled: boolean;
      autoExpunge?: boolean;
      expungeBehavior?: string;
    };
    pop?: {
      enabled: boolean;
      accessWindow?: string;
    };
    vacationResponder?: {
      enabled: boolean;
      startTime?: string;
      endTime?: string;
      responseSubject?: string;
      message?: string;
    };
  };
}

export interface GmailModuleConfig {
  clientId: string;
  clientSecret: string;
  redirectUri: string;
  scopes: string[];
}

export interface SearchCriteria {
  from?: string | string[];
  to?: string | string[];
  subject?: string;
  content?: string;
  after?: string;
  before?: string;
  hasAttachment?: boolean;
  labels?: string[];
  excludeLabels?: string[];
  includeSpam?: boolean;
  isUnread?: boolean;
}

export interface EmailResponse {
  id: string;
  threadId: string;
  labelIds?: string[];
  snippet?: string;
  subject: string;
  from: string;
  to: string;
  date: string;
  body: string;
  headers?: { [key: string]: string };
  isUnread: boolean;
  hasAttachment: boolean;
  attachments?: IncomingGmailAttachment[];
}

export interface ThreadInfo {
  messages: string[];
  participants: string[];
  subject: string;
  lastUpdated: string;
}

export interface GetEmailsParams {
  email: string;
  search?: SearchCriteria;
  options?: {
    maxResults?: number;
    pageToken?: string;
    format?: 'full' | 'metadata' | 'minimal';
    includeHeaders?: boolean;
    threadedView?: boolean;
    sortOrder?: 'asc' | 'desc';
  };
  messageIds?: string[];
}

export interface GetEmailsResponse {
  emails: EmailResponse[];
  nextPageToken?: string;
  resultSummary: {
    total: number;
    returned: number;
    hasMore: boolean;
    searchCriteria: SearchCriteria;
  };
  threads?: { [threadId: string]: ThreadInfo };
}

export interface SendEmailParams {
  email: string;
  to: string[];
  subject: string;
  body: string;
  cc?: string[];
  bcc?: string[];
  attachments?: OutgoingGmailAttachment[];
}

export interface SendEmailResponse {
  messageId: string;
  threadId: string;
  labelIds?: string[];
  attachments?: GmailAttachment[];
}

export class GmailError extends Error implements GmailError {
  code: string;
  details?: string;

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

```

--------------------------------------------------------------------------------
/src/__helpers__/testSetup.ts:
--------------------------------------------------------------------------------

```typescript
import { gmail_v1 } from 'googleapis';
import type { AccountManager } from '../modules/accounts/manager.js' with { "resolution-mode": "import" };
import type { OAuth2Client } from 'google-auth-library' with { "resolution-mode": "import" };

// Basic mock token
const mockToken = {
  access_token: 'mock-access-token',
  refresh_token: 'mock-refresh-token',
  expiry_date: Date.now() + 3600000,
};

// Simple account manager mock
export const mockAccountManager = (): jest.Mocked<Partial<AccountManager>> => {
  const mockAuthClient: jest.Mocked<Partial<OAuth2Client>> = {
    setCredentials: jest.fn(),
  };

  return {
    initialize: jest.fn().mockResolvedValue(undefined),
    getAuthClient: jest.fn().mockResolvedValue(mockAuthClient),
    validateToken: jest.fn().mockResolvedValue({ valid: true, token: mockToken }),
    withTokenRenewal: jest.fn().mockImplementation((email, operation) => operation()),
  };
};

// Type for Gmail client that makes context optional but keeps users required
type MockGmailClient = Omit<gmail_v1.Gmail, 'context'> & { context?: gmail_v1.Gmail['context'] };

// Simple Gmail client mock with proper typing and success/failure cases
export const mockGmailClient: jest.Mocked<MockGmailClient> = {
  users: {
    messages: {
      list: jest.fn()
        .mockResolvedValueOnce({ // Success case with results
          data: {
            messages: [
              { id: 'msg-1', threadId: 'thread-1' },
              { id: 'msg-2', threadId: 'thread-1' }
            ],
            resultSizeEstimate: 2
          }
        })
        .mockResolvedValueOnce({ // Empty results case
          data: { 
            messages: [],
            resultSizeEstimate: 0
          }
        }),
      get: jest.fn().mockResolvedValue({
        data: {
          id: 'msg-1',
          threadId: 'thread-1',
          labelIds: ['INBOX'],
          snippet: 'Email preview...',
          payload: {
            headers: [
              { name: 'Subject', value: 'Test Subject' },
              { name: 'From', value: '[email protected]' },
              { name: 'To', value: '[email protected]' },
              { name: 'Date', value: new Date().toISOString() }
            ],
            body: {
              data: Buffer.from('Email body content').toString('base64')
            }
          }
        }
      }),
      send: jest.fn()
        .mockResolvedValueOnce({ // Success case
          data: {
            id: 'sent-msg-1',
            threadId: 'thread-1',
            labelIds: ['SENT']
          }
        })
        .mockRejectedValueOnce(new Error('Send failed')), // Error case
    },
    drafts: {
      create: jest.fn()
        .mockResolvedValueOnce({ // Success case
          data: {
            id: 'draft-1',
            message: {
              id: 'msg-draft-1',
              threadId: 'thread-1',
              labelIds: ['DRAFT']
            },
            updated: new Date().toISOString()
          }
        })
        .mockResolvedValueOnce({ // Reply draft success case
          data: {
            id: 'draft-2',
            message: {
              id: 'msg-draft-2',
              threadId: 'thread-1',
              labelIds: ['DRAFT']
            },
            updated: new Date().toISOString()
          }
        })
        .mockRejectedValueOnce(new Error('Draft creation failed')), // Error case
      list: jest.fn()
        .mockResolvedValueOnce({ // Success case with drafts
          data: {
            drafts: [
              { id: 'draft-1' },
              { id: 'draft-2' }
            ],
            nextPageToken: 'next-token',
            resultSizeEstimate: 2
          }
        })
        .mockResolvedValueOnce({ // Empty drafts case
          data: {
            drafts: [],
            resultSizeEstimate: 0
          }
        }),
      get: jest.fn()
        .mockResolvedValue({ // Success case for draft details
          data: {
            id: 'draft-1',
            message: {
              id: 'msg-draft-1',
              threadId: 'thread-1',
              labelIds: ['DRAFT']
            }
          }
        }),
      send: jest.fn()
        .mockResolvedValueOnce({ // Success case
          data: {
            id: 'sent-msg-1',
            threadId: 'thread-1',
            labelIds: ['SENT']
          }
        })
        .mockRejectedValueOnce(new Error('Draft send failed')), // Error case
    },
    getProfile: jest.fn()
      .mockResolvedValueOnce({ // Success case
        data: {
          emailAddress: '[email protected]',
          messagesTotal: 100,
          threadsTotal: 50,
          historyId: '12345'
        }
      })
      .mockRejectedValueOnce(new Error('Settings fetch failed')), // Error case
    settings: {
      getAutoForwarding: jest.fn().mockResolvedValue({
        data: {
          enabled: false,
          emailAddress: undefined
        }
      }),
      getImap: jest.fn().mockResolvedValue({
        data: {
          enabled: true,
          autoExpunge: true,
          expungeBehavior: 'archive'
        }
      }),
      getLanguage: jest.fn().mockResolvedValue({
        data: {
          displayLanguage: 'en'
        }
      }),
      getPop: jest.fn().mockResolvedValue({
        data: {
          enabled: false,
          accessWindow: 'disabled'
        }
      }),
      getVacation: jest.fn().mockResolvedValue({
        data: {
          enabled: false,
          startTime: undefined,
          endTime: undefined,
          responseSubject: '',
          message: ''
        }
      }),
    },
  } as any,
};

// Simple file system mock
export const mockFileSystem = () => {
  const fs = {
    mkdir: jest.fn().mockResolvedValue(undefined),
    readFile: jest.fn().mockResolvedValue(Buffer.from('test content')),
    writeFile: jest.fn().mockResolvedValue(undefined),
    unlink: jest.fn().mockResolvedValue(undefined),
    access: jest.fn().mockResolvedValue(undefined),
    chmod: jest.fn().mockResolvedValue(undefined),
  };

  jest.mock('fs/promises', () => fs);

  return { fs };
};

// Mock the account manager module
jest.mock('../modules/accounts/index.js', () => ({
  initializeAccountModule: jest.fn().mockResolvedValue(mockAccountManager()),
  getAccountManager: jest.fn().mockReturnValue(mockAccountManager()),
}));

// Mock googleapis
jest.mock('googleapis', () => {
  const mockDrive = {
    files: {
      list: jest.fn(),
      create: jest.fn(),
      get: jest.fn(),
      delete: jest.fn(),
      export: jest.fn(),
    },
    permissions: {
      create: jest.fn(),
    },
  };

  return {
    google: {
      gmail: jest.fn().mockReturnValue(mockGmailClient),
      drive: jest.fn().mockReturnValue(mockDrive),
    },
  };
});

// Reset mocks between tests
beforeEach(() => {
  jest.clearAllMocks();
});

```

--------------------------------------------------------------------------------
/src/modules/attachments/service.ts:
--------------------------------------------------------------------------------

```typescript
import {
  AttachmentMetadata,
  AttachmentResult,
  AttachmentServiceConfig,
  AttachmentSource,
  AttachmentValidationResult,
  ATTACHMENT_FOLDERS,
  AttachmentFolderType
} from './types.js';
import fs from 'fs/promises';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';

const DEFAULT_CONFIG: AttachmentServiceConfig = {
  maxSizeBytes: 25 * 1024 * 1024, // 25MB
  allowedMimeTypes: ['*/*'],
  quotaLimitBytes: 1024 * 1024 * 1024, // 1GB
  basePath: process.env.WORKSPACE_BASE_PATH ? 
    path.join(process.env.WORKSPACE_BASE_PATH, ATTACHMENT_FOLDERS.ROOT) : 
    '/app/workspace/attachments'
};

export class AttachmentService {
  private static instance: AttachmentService;
  private config: AttachmentServiceConfig;
  private initialized = false;

  private constructor(config: AttachmentServiceConfig = {}) {
    this.config = { ...DEFAULT_CONFIG, ...config };
  }

  /**
   * Get the singleton instance
   */
  public static getInstance(config: AttachmentServiceConfig = {}): AttachmentService {
    if (!AttachmentService.instance) {
      AttachmentService.instance = new AttachmentService(config);
    }
    return AttachmentService.instance;
  }

  /**
   * Initialize attachment folders in local storage
   */
  async initialize(email: string): Promise<void> {
    try {
      // Create base attachment directory
      await fs.mkdir(this.config.basePath!, { recursive: true });

      // Create email directory structure
      const emailPath = path.join(this.config.basePath!, ATTACHMENT_FOLDERS.EMAIL);
      await fs.mkdir(emailPath, { recursive: true });
      await fs.mkdir(path.join(emailPath, ATTACHMENT_FOLDERS.INCOMING), { recursive: true });
      await fs.mkdir(path.join(emailPath, ATTACHMENT_FOLDERS.OUTGOING), { recursive: true });

      // Create calendar directory structure
      const calendarPath = path.join(this.config.basePath!, ATTACHMENT_FOLDERS.CALENDAR);
      await fs.mkdir(calendarPath, { recursive: true });
      await fs.mkdir(path.join(calendarPath, ATTACHMENT_FOLDERS.EVENT_FILES), { recursive: true });

      this.initialized = true;
    } catch (error) {
      throw new Error(`Failed to initialize attachment directories: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  /**
   * Validate attachment against configured limits
   */
  private validateAttachment(source: AttachmentSource): AttachmentValidationResult {
    // Check size if available
    if (source.metadata.size && this.config.maxSizeBytes) {
      if (source.metadata.size > this.config.maxSizeBytes) {
        return {
          valid: false,
          error: `File size ${source.metadata.size} exceeds maximum allowed size ${this.config.maxSizeBytes}`
        };
      }
    }

    // Check MIME type if restricted
    if (this.config.allowedMimeTypes && 
        this.config.allowedMimeTypes[0] !== '*/*' &&
        !this.config.allowedMimeTypes.includes(source.metadata.mimeType)) {
      return {
        valid: false,
        error: `MIME type ${source.metadata.mimeType} is not allowed`
      };
    }

    return { valid: true };
  }

  /**
   * Process attachment and store in local filesystem
   */
  async processAttachment(
    email: string,
    source: AttachmentSource,
    parentFolder: string
  ): Promise<AttachmentResult> {
    if (!this.initialized) {
      await this.initialize(email);
    }

    // Validate attachment
    const validation = this.validateAttachment(source);
    if (!validation.valid) {
      return {
        success: false,
        error: validation.error
      };
    }

    try {
      if (!source.content) {
        throw new Error('File content not provided');
      }

      // Generate unique ID and create file path
      const id = uuidv4();
      const folderPath = path.join(this.config.basePath!, parentFolder);
      const filePath = path.join(folderPath, `${id}_${source.metadata.name}`);

      // Write file content
      const content = Buffer.from(source.content, 'base64');
      await fs.writeFile(filePath, content);

      // Get actual file size
      const stats = await fs.stat(filePath);

      return {
        success: true,
        attachment: {
          id,
          name: source.metadata.name,
          mimeType: source.metadata.mimeType,
          size: stats.size,
          path: filePath
        }
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error occurred'
      };
    }
  }

  /**
   * Download attachment from local storage
   */
  async downloadAttachment(
    email: string,
    attachmentId: string,
    filePath: string
  ): Promise<AttachmentResult> {
    if (!this.initialized) {
      await this.initialize(email);
    }

    try {
      // Verify file exists and is within workspace
      if (!filePath.startsWith(this.config.basePath!)) {
        throw new Error('Invalid file path');
      }

      const content = await fs.readFile(filePath);
      const stats = await fs.stat(filePath);

      return {
        success: true,
        attachment: {
          id: attachmentId,
          name: path.basename(filePath).substring(37), // Remove UUID prefix
          mimeType: path.extname(filePath) ? 
            `application/${path.extname(filePath).substring(1)}` : 
            'application/octet-stream',
          size: stats.size,
          path: filePath
        }
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error occurred'
      };
    }
  }

  /**
   * Delete attachment from local storage
   */
  async deleteAttachment(
    email: string,
    attachmentId: string,
    filePath: string
  ): Promise<AttachmentResult> {
    if (!this.initialized) {
      await this.initialize(email);
    }

    try {
      // Verify file exists and is within workspace
      if (!filePath.startsWith(this.config.basePath!)) {
        throw new Error('Invalid file path');
      }

      // Get file stats before deletion
      const stats = await fs.stat(filePath);
      const name = path.basename(filePath).substring(37); // Remove UUID prefix
      const mimeType = path.extname(filePath) ? 
        `application/${path.extname(filePath).substring(1)}` : 
        'application/octet-stream';

      // Delete the file
      await fs.unlink(filePath);

      return {
        success: true,
        attachment: {
          id: attachmentId,
          name,
          mimeType,
          size: stats.size,
          path: filePath
        }
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error occurred'
      };
    }
  }

  /**
   * Get full path for a specific attachment category
   */
  getAttachmentPath(folder: AttachmentFolderType): string {
    return path.join(this.config.basePath!, folder);
  }
}

```

--------------------------------------------------------------------------------
/src/tools/calendar-handlers.ts:
--------------------------------------------------------------------------------

```typescript
import { CalendarService } from '../modules/calendar/service.js';
import { DriveService } from '../modules/drive/service.js';
import { validateEmail } from '../utils/account.js';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { getAccountManager } from '../modules/accounts/index.js';
import { CalendarError } from '../modules/calendar/types.js';

// Singleton instances
let driveService: DriveService;
let calendarService: CalendarService;
let accountManager: ReturnType<typeof getAccountManager>;

const CALENDAR_CONFIG = {
  maxAttachmentSize: 10 * 1024 * 1024, // 10MB
  allowedAttachmentTypes: [
    'application/pdf',
    'application/msword',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    'application/vnd.ms-excel',
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    'application/vnd.ms-powerpoint',
    'application/vnd.openxmlformats-officedocument.presentationml.presentation',
    'image/jpeg',
    'image/png',
    'text/plain'
  ]
};

// Initialize services lazily
async function initializeServices() {
  if (!driveService) {
    driveService = new DriveService();
  }
  if (!calendarService) {
    calendarService = new CalendarService(CALENDAR_CONFIG);
    await calendarService.ensureInitialized();
  }
  if (!accountManager) {
    accountManager = getAccountManager();
  }
}

export async function handleListWorkspaceCalendarEvents(params: any) {
  await initializeServices();
  const { email, query, maxResults, timeMin, timeMax } = params;

  if (!email) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'Email address is required'
    );
  }

  validateEmail(email);

  return accountManager.withTokenRenewal(email, async () => {
    try {
      return await calendarService.getEvents({
        email,
        query,
        maxResults,
        timeMin,
        timeMax
      });
    } catch (error) {
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to list calendar events: ${error instanceof Error ? error.message : 'Unknown error'}`
      );
    }
  });
}

export async function handleGetWorkspaceCalendarEvent(params: any) {
  await initializeServices();
  const { email, eventId } = params;

  if (!email) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'Email address is required'
    );
  }

  if (!eventId) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'Event ID is required'
    );
  }

  validateEmail(email);

  return accountManager.withTokenRenewal(email, async () => {
    try {
      return await calendarService.getEvent(email, eventId);
    } catch (error) {
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to get calendar event: ${error instanceof Error ? error.message : 'Unknown error'}`
      );
    }
  });
}

export async function handleCreateWorkspaceCalendarEvent(params: any) {
  await initializeServices();
  const { email, summary, description, start, end, attendees, attachments } = params;

  if (!email) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'Email address is required'
    );
  }

  if (!summary) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'Event summary is required'
    );
  }

  if (!start || !start.dateTime) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'Event start time is required'
    );
  }

  if (!end || !end.dateTime) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'Event end time is required'
    );
  }

  validateEmail(email);
  if (attendees) {
    attendees.forEach((attendee: { email: string }) => validateEmail(attendee.email));
  }

  return accountManager.withTokenRenewal(email, async () => {
    try {
      return await calendarService.createEvent({
        email,
        summary,
        description,
        start,
        end,
        attendees,
        attachments: attachments?.map((attachment: {
          driveFileId?: string;
          content?: string;
          name: string;
          mimeType: string;
          size?: number;
        }) => ({
          driveFileId: attachment.driveFileId,
          content: attachment.content,
          name: attachment.name,
          mimeType: attachment.mimeType,
          size: attachment.size
        }))
      });
    } catch (error) {
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to create calendar event: ${error instanceof Error ? error.message : 'Unknown error'}`
      );
    }
  });
}

export async function handleManageWorkspaceCalendarEvent(params: any) {
  await initializeServices();
  const { email, eventId, action, comment, newTimes } = params;

  if (!email) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'Email address is required'
    );
  }

  if (!eventId) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'Event ID is required'
    );
  }

  if (!action) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'Action is required'
    );
  }

  validateEmail(email);

  return accountManager.withTokenRenewal(email, async () => {
    try {
      return await calendarService.manageEvent({
        email,
        eventId,
        action,
        comment,
        newTimes
      });
    } catch (error) {
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to manage calendar event: ${error instanceof Error ? error.message : 'Unknown error'}`
      );
    }
  });
}

export async function handleDeleteWorkspaceCalendarEvent(params: any) {
  await initializeServices();
  const { email, eventId, sendUpdates, deletionScope } = params;

  if (!email) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'Email address is required'
    );
  }

  if (!eventId) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'Event ID is required'
    );
  }

  // Validate deletionScope if provided
  if (deletionScope && !['entire_series', 'this_and_following'].includes(deletionScope)) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'Invalid deletion scope. Must be one of: entire_series, this_and_following'
    );
  }

  validateEmail(email);

  return accountManager.withTokenRenewal(email, async () => {
    try {
      await calendarService.deleteEvent(email, eventId, sendUpdates, deletionScope);
      // Return a success response object instead of void
      return {
        status: 'success',
        message: 'Event deleted successfully',
        details: deletionScope ? 
          `Event deleted with scope: ${deletionScope}` : 
          'Event deleted completely'
      };
    } catch (error) {
      // Check if this is a CalendarError with a specific code
      if (error instanceof CalendarError) {
        throw new McpError(
          ErrorCode.InvalidParams,
          error.message,
          error.details
        );
      }
      
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to delete calendar event: ${error instanceof Error ? error.message : 'Unknown error'}`
      );
    }
  });
}

```

--------------------------------------------------------------------------------
/src/tools/types.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Standard response format for all MCP tools
 * @property content - Array of content blocks to return to the client
 * @property isError - Whether this response represents an error condition
 * @property _meta - Optional metadata about the response
 */
export interface McpToolResponse {
  content: {
    type: 'text';
    text: string;
  }[];
  isError?: boolean;
  _meta?: Record<string, unknown>;
}

/**
 * Base interface for all tool arguments
 * Provides type safety for unknown properties
 */
export interface ToolArguments {
  [key: string]: unknown;
}

/**
 * Common arguments required by all workspace tools
 * @property email - The Google Workspace account email address
 */
export interface BaseToolArguments extends Record<string, unknown> {
  email: string;
}

/**
 * Parameters for authenticating a workspace account
 * @property email - Email address to authenticate
 * @property category - Optional account category (e.g., 'work', 'personal')
 * @property description - Optional account description
 * @property auth_code - OAuth2 authorization code (required for completing auth)
 */
export interface AuthenticateAccountArgs extends BaseToolArguments {
  category?: string;
  description?: string;
  auth_code?: string;
}

// Gmail Types
/**
 * Parameters for searching Gmail messages
 * @property email - The Gmail account to search
 * @property search - Search criteria for filtering emails
 * @property options - Search options including pagination and format
 */
export interface GmailSearchParams {
  email: string;
  search?: {
    from?: string | string[];
    to?: string | string[];
    subject?: string;
    content?: string;
    after?: string; // YYYY-MM-DD
    before?: string; // YYYY-MM-DD
    hasAttachment?: boolean;
    labels?: string[]; // e.g., ["INBOX", "IMPORTANT"]
    excludeLabels?: string[];
    includeSpam?: boolean;
    isUnread?: boolean;
  };
  options?: {
    maxResults?: number;
    pageToken?: string;
    includeHeaders?: boolean;
    format?: 'full' | 'metadata' | 'minimal';
    threadedView?: boolean;
    sortOrder?: 'asc' | 'desc';
  };
}

/**
 * Content for sending or creating draft emails
 * @property to - Array of recipient email addresses
 * @property subject - Email subject line
 * @property body - Email body content (supports HTML)
 * @property cc - Optional CC recipients
 * @property bcc - Optional BCC recipients
 */
export interface EmailContent {
  to: string[];
  subject: string;
  body: string;
  cc?: string[];
  bcc?: string[];
}

/**
 * Parameters for sending emails
 */
export interface SendEmailArgs extends BaseToolArguments, EmailContent {}

// Calendar Types
/**
 * Parameters for listing calendar events
 * @property email - Calendar owner's email
 * @property query - Optional text search within events
 * @property maxResults - Maximum events to return
 * @property timeMin - Start of time range (ISO-8601)
 * @property timeMax - End of time range (ISO-8601)
 */
export interface CalendarEventParams extends BaseToolArguments {
  query?: string;
  maxResults?: number; // Default: 10
  timeMin?: string; // ISO-8601 format
  timeMax?: string; // ISO-8601 format
}

/**
 * Time specification for calendar events
 * @property dateTime - Event time in ISO-8601 format
 * @property timeZone - IANA timezone identifier
 */
export interface CalendarEventTime {
  dateTime: string; // e.g., "2024-02-18T15:30:00-06:00"
  timeZone?: string; // e.g., "America/Chicago"
}

/**
 * Calendar event attendee information
 * @property email - Attendee's email address
 */
export interface CalendarEventAttendee {
  email: string;
}

// Drive Types
/**
 * Parameters for listing Drive files
 */
export interface DriveFileListArgs extends BaseToolArguments {
  options?: {
    folderId?: string;
    query?: string;
    pageSize?: number;
    orderBy?: string[];
    fields?: string[];
  };
}

/**
 * Parameters for searching Drive files
 */
export interface DriveSearchArgs extends BaseToolArguments {
  options: {
    fullText?: string;
    mimeType?: string;
    folderId?: string;
    trashed?: boolean;
    query?: string;
    pageSize?: number;
  };
}

/**
 * Parameters for uploading files to Drive
 */
export interface DriveUploadArgs extends BaseToolArguments {
  options: {
    name: string;
    content: string;
    mimeType?: string;
    parents?: string[];
  };
}

/**
 * Parameters for downloading files from Drive
 */
export interface DriveDownloadArgs extends BaseToolArguments {
  fileId: string;
  mimeType?: string;
}

/**
 * Parameters for creating Drive folders
 */
export interface DriveFolderArgs extends BaseToolArguments {
  name: string;
  parentId?: string;
}

/**
 * Parameters for updating Drive permissions
 */
export interface DrivePermissionArgs extends BaseToolArguments {
  options: {
    fileId: string;
    role: 'owner' | 'organizer' | 'fileOrganizer' | 'writer' | 'commenter' | 'reader';
    type: 'user' | 'group' | 'domain' | 'anyone';
    emailAddress?: string;
    domain?: string;
    allowFileDiscovery?: boolean;
  };
}

/**
 * Parameters for deleting Drive files
 */
export interface DriveDeleteArgs extends BaseToolArguments {
  fileId: string;
}

// Label Types
/**
 * Color settings for Gmail labels
 * @property textColor - Hex color code for label text
 * @property backgroundColor - Hex color code for label background
 */
export interface LabelColor {
  textColor: string; // e.g., "#000000"
  backgroundColor: string; // e.g., "#FFFFFF"
}

/**
 * Parameters for creating labels
 */
export interface CreateLabelArgs extends BaseToolArguments {
  name: string;
  messageListVisibility?: 'show' | 'hide';
  labelListVisibility?: 'labelShow' | 'labelHide' | 'labelShowIfUnread';
  color?: LabelColor;
}

/**
 * Parameters for updating labels
 */
export interface UpdateLabelArgs extends CreateLabelArgs {
  labelId: string;
}

/**
 * Parameters for deleting labels
 */
export interface DeleteLabelArgs extends BaseToolArguments {
  labelId: string;
}

/**
 * Parameters for modifying message labels
 */
export interface ModifyLabelsArgs extends BaseToolArguments {
  messageId: string;
  addLabelIds?: string[];
  removeLabelIds?: string[];
}

// Label Filter Types
/**
 * Filter criteria for matching emails
 */
export interface LabelFilterCriteria {
  from?: string[];
  to?: string[];
  subject?: string;
  hasWords?: string[];
  doesNotHaveWords?: string[];
  hasAttachment?: boolean;
  size?: {
    operator: 'larger' | 'smaller';
    size: number;
  };
}

/**
 * Actions to take when filter matches
 */
export interface LabelFilterActions {
  addLabel: boolean;
  markImportant?: boolean;
  markRead?: boolean;
  archive?: boolean;
}

/**
 * Parameters for creating label filters
 */
export interface CreateLabelFilterArgs extends BaseToolArguments {
  labelId: string;
  criteria: LabelFilterCriteria;
  actions: LabelFilterActions;
}

/**
 * Parameters for getting label filters
 */
export interface GetLabelFiltersArgs extends BaseToolArguments {
  labelId?: string;  // Optional: get filters for specific label
}

/**
 * Parameters for updating label filters
 */
export interface UpdateLabelFilterArgs extends BaseToolArguments {
  filterId: string;
  criteria?: LabelFilterCriteria;
  actions?: LabelFilterActions;
}

/**
 * Parameters for deleting label filters
 */
export interface DeleteLabelFilterArgs extends BaseToolArguments {
  filterId: string;
}

// Attachment Management Types
export interface ManageAttachmentParams extends BaseToolArguments {
  action: 'download' | 'upload' | 'delete';
  source: 'email' | 'calendar';
  messageId: string;
  filename: string;
  content?: string;  // Required for upload action
}

// Re-export consolidated management types
export {
  ManageLabelParams,
  ManageLabelAssignmentParams,
  ManageLabelFilterParams
} from '../modules/gmail/services/label.js';

export {
  ManageDraftParams,
  DraftAction
} from '../modules/gmail/services/draft.js';

```

--------------------------------------------------------------------------------
/src/modules/drive/__tests__/service.test.ts:
--------------------------------------------------------------------------------

```typescript
import { DriveService } from '../service.js';
import { getAccountManager } from '../../accounts/index.js';
import { DRIVE_SCOPES } from '../scopes.js';
import { GoogleServiceError } from '../../../services/base/BaseGoogleService.js';
import { mockFileSystem } from '../../../__helpers__/testSetup.js';

jest.mock('../../accounts/index.js');
jest.mock('googleapis');
jest.mock('../../../utils/workspace.js', () => ({
  workspaceManager: {
    getUploadPath: jest.fn().mockResolvedValue('/tmp/test-upload.txt'),
    getDownloadPath: jest.fn().mockResolvedValue('/tmp/test-download.txt'),
    initializeAccountDirectories: jest.fn().mockResolvedValue(undefined)
  }
}));

const { fs } = mockFileSystem();

describe('DriveService', () => {
  const testEmail = '[email protected]';
  let service: DriveService;
  let mockDrive: any;

  beforeEach(async () => {
    jest.clearAllMocks();

    // Mock file system operations
    fs.writeFile.mockResolvedValue(undefined);
    fs.readFile.mockResolvedValue(Buffer.from('test content'));
    
    // Mock account manager
    (getAccountManager as jest.Mock).mockReturnValue({
      validateToken: jest.fn().mockResolvedValue({
        valid: true,
        token: { access_token: 'test-token' },
        requiredScopes: Object.values(DRIVE_SCOPES)
      }),
      getAuthClient: jest.fn().mockResolvedValue({
        setCredentials: jest.fn()
      })
    });

    const { google } = require('googleapis');
    mockDrive = {
      files: {
        list: jest.fn(),
        create: jest.fn(),
        get: jest.fn(),
        delete: jest.fn(),
        export: jest.fn()
      },
      permissions: {
        create: jest.fn()
      }
    };
    google.drive.mockReturnValue(mockDrive);
    
    service = new DriveService();
  });

  describe('listFiles', () => {
    it('should list files successfully', async () => {
      const mockResponse = {
        data: {
          files: [
            { id: '1', name: 'test.txt' }
          ]
        }
      };

      mockDrive.files.list.mockResolvedValue(mockResponse);

      const result = await service.listFiles(testEmail);

      expect(result.success).toBe(true);
      expect(result.data).toEqual(mockResponse.data);
      expect(mockDrive.files.list).toHaveBeenCalled();
    });

    it('should handle errors', async () => {
      mockDrive.files.list.mockRejectedValue(new Error('API error'));

      const result = await service.listFiles(testEmail);

      expect(result.success).toBe(false);
      expect(result.error).toBe('API error');
    });
  });

  describe('uploadFile', () => {
    it('should upload file successfully', async () => {
      const mockResponse = {
        data: {
          id: '1',
          name: 'test.txt',
          mimeType: 'text/plain',
          webViewLink: 'https://drive.google.com/file/d/1'
        }
      };

      mockDrive.files.create.mockResolvedValue(mockResponse);

      const result = await service.uploadFile(testEmail, {
        name: 'test.txt',
        content: 'test content',
        mimeType: 'text/plain'
      });

      expect(result.success).toBe(true);
      expect(result.data).toEqual(mockResponse.data);
      expect(mockDrive.files.create).toHaveBeenCalledWith(expect.objectContaining({
        requestBody: {
          name: 'test.txt',
          mimeType: 'text/plain'
        },
        fields: 'id, name, mimeType, webViewLink'
      }));
    });

    it('should handle upload errors', async () => {
      mockDrive.files.create.mockRejectedValue(new Error('Upload failed'));

      const result = await service.uploadFile(testEmail, {
        name: 'test.txt',
        content: 'test content'
      });

      expect(result.success).toBe(false);
      expect(result.error).toBe('Upload failed');
    });
  });

  describe('downloadFile', () => {
    // Simplified to a single test case
    it('should handle download operations', async () => {
      const mockMetadata = {
        data: {
          name: 'test.txt',
          mimeType: 'text/plain'
        }
      };
      mockDrive.files.get.mockResolvedValue(mockMetadata);

      const result = await service.downloadFile(testEmail, {
        fileId: '1'
      });

      expect(result.success).toBeDefined();
      expect(mockDrive.files.get).toHaveBeenCalled();
    });
  });

  describe('searchFiles', () => {
    // Simplified to basic functionality test
    it('should handle search operations', async () => {
      const mockResponse = {
        data: {
          files: [{ id: '1', name: 'test.txt' }]
        }
      };
      mockDrive.files.list.mockResolvedValue(mockResponse);

      const result = await service.searchFiles(testEmail, {
        fullText: 'test'
      });

      expect(result.success).toBeDefined();
      expect(mockDrive.files.list).toHaveBeenCalled();
    });
  });

  describe('updatePermissions', () => {
    it('should update permissions successfully', async () => {
      const mockResponse = {
        data: {
          id: '1',
          type: 'user',
          role: 'reader'
        }
      };

      mockDrive.permissions.create.mockResolvedValue(mockResponse);

      const result = await service.updatePermissions(testEmail, {
        fileId: '1',
        type: 'user',
        role: 'reader',
        emailAddress: '[email protected]'
      });

      expect(result.success).toBe(true);
      expect(result.data).toEqual(mockResponse.data);
      expect(mockDrive.permissions.create).toHaveBeenCalledWith({
        fileId: '1',
        requestBody: {
          role: 'reader',
          type: 'user',
          emailAddress: '[email protected]'
        }
      });
    });

    it('should handle permission update errors', async () => {
      mockDrive.permissions.create.mockRejectedValue(new Error('Permission update failed'));

      const result = await service.updatePermissions(testEmail, {
        fileId: '1',
        type: 'user',
        role: 'reader',
        emailAddress: '[email protected]'
      });

      expect(result.success).toBe(false);
      expect(result.error).toBe('Permission update failed');
    });
  });

  describe('deleteFile', () => {
    it('should delete file successfully', async () => {
      mockDrive.files.delete.mockResolvedValue({});

      const result = await service.deleteFile(testEmail, '1');

      expect(result.success).toBe(true);
      expect(mockDrive.files.delete).toHaveBeenCalledWith({
        fileId: '1'
      });
    });

    it('should handle delete errors', async () => {
      mockDrive.files.delete.mockRejectedValue(new Error('Delete failed'));

      const result = await service.deleteFile(testEmail, '1');

      expect(result.success).toBe(false);
      expect(result.error).toBe('Delete failed');
    });
  });

  describe('createFolder', () => {
    it('should create folder successfully', async () => {
      const mockResponse = {
        data: {
          id: '1',
          name: 'Test Folder',
          mimeType: 'application/vnd.google-apps.folder',
          webViewLink: 'https://drive.google.com/drive/folders/1'
        }
      };

      mockDrive.files.create.mockResolvedValue(mockResponse);

      const result = await service.createFolder(testEmail, 'Test Folder', 'parent123');

      expect(result.success).toBe(true);
      expect(result.data).toEqual(mockResponse.data);
      expect(mockDrive.files.create).toHaveBeenCalledWith({
        requestBody: {
          name: 'Test Folder',
          mimeType: 'application/vnd.google-apps.folder',
          parents: ['parent123']
        },
        fields: 'id, name, mimeType, webViewLink'
      });
    });

    it('should handle folder creation errors', async () => {
      mockDrive.files.create.mockRejectedValue(new Error('Folder creation failed'));

      const result = await service.createFolder(testEmail, 'Test Folder');

      expect(result.success).toBe(false);
      expect(result.error).toBe('Folder creation failed');
    });
  });
});

```

--------------------------------------------------------------------------------
/src/__tests__/modules/gmail/service.test.ts:
--------------------------------------------------------------------------------

```typescript
import { GmailService } from '../../../modules/gmail/service.js';
import { gmail_v1 } from 'googleapis';
import { getAccountManager } from '../../../modules/accounts/index.js';
import { AccountManager } from '../../../modules/accounts/manager.js';
import { DraftResponse, GetDraftsResponse, SendEmailResponse } from '../../../modules/gmail/types.js';

jest.mock('../../../modules/accounts/index.js');
jest.mock('../../../modules/accounts/manager.js');

describe('GmailService', () => {
  let gmailService: GmailService;
  let mockGmailClient: jest.Mocked<gmail_v1.Gmail>;
  let mockAccountManager: jest.Mocked<AccountManager>;
  const testEmail = '[email protected]';

  beforeEach(async () => {
    // Simplified mock setup
    mockGmailClient = {
      users: {
        messages: {
          list: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
          get: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
          send: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
        },
        drafts: {
          create: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
          list: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
          get: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
          send: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
        },
        getProfile: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
        settings: {
          getAutoForwarding: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
          getImap: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
          getLanguage: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
          getPop: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
          getVacation: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
        },
      },
    } as any;

    mockAccountManager = {
      validateToken: jest.fn().mockResolvedValue({ valid: true, token: {} }),
      getAuthClient: jest.fn().mockResolvedValue({}),
      withTokenRenewal: jest.fn().mockImplementation((email, operation) => operation()),
    } as unknown as jest.Mocked<AccountManager>;

    (getAccountManager as jest.Mock).mockReturnValue(mockAccountManager);

    gmailService = new GmailService();
    await gmailService.initialize();

    // Mock the base service's getAuthenticatedClient method
    (gmailService as any).getAuthenticatedClient = jest.fn().mockResolvedValue(mockGmailClient);

    // Mock all internal services
    (gmailService as any).emailService.gmailClient = mockGmailClient;
    (gmailService as any).emailService.getAuthenticatedClient = jest.fn().mockResolvedValue(mockGmailClient);
    
    (gmailService as any).draftService.gmailClient = mockGmailClient;
    (gmailService as any).draftService.getAuthenticatedClient = jest.fn().mockResolvedValue(mockGmailClient);
    
    (gmailService as any).settingsService.gmailClient = mockGmailClient;
    (gmailService as any).settingsService.getAuthenticatedClient = jest.fn().mockResolvedValue(mockGmailClient);
  });

  describe('getEmails', () => {
    it('should get emails with search criteria', async () => {
      const mockMessages = [
        { id: 'msg1', threadId: 'thread1' },
        { id: 'msg2', threadId: 'thread2' }
      ];

      (mockGmailClient.users.messages.list as jest.Mock).mockImplementation(() =>
        Promise.resolve({ data: { messages: mockMessages, resultSizeEstimate: 2 } })
      );

      const result = await gmailService.getEmails({
        email: testEmail,
        search: { subject: 'test' }
      });

      expect(result.emails.length).toBe(2);
      expect(result.resultSummary.total).toBe(2);
    });

    it('should handle empty results', async () => {
      (mockGmailClient.users.messages.list as jest.Mock).mockImplementation(() =>
        Promise.resolve({ data: { messages: [], resultSizeEstimate: 0 } })
      );

      const result = await gmailService.getEmails({ email: testEmail });
      expect(result.emails).toEqual([]);
      expect(result.resultSummary.total).toBe(0);
    });
  });

  describe('sendEmail', () => {
    const emailParams = {
      email: testEmail,
      to: ['[email protected]'],
      subject: 'Test',
      body: 'Hello'
    };

    it('should send email', async () => {
      (mockGmailClient.users.messages.send as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: { id: 'msg1', threadId: 'thread1', labelIds: ['SENT'] }
        })
      );

      const result = await gmailService.sendEmail(emailParams);
      expect(result.messageId).toBe('msg1');
      expect(result.labelIds).toContain('SENT');
    });

    it('should handle send failure', async () => {
      (mockGmailClient.users.messages.send as jest.Mock).mockImplementation(() =>
        Promise.reject(new Error('Send failed'))
      );

      await expect(gmailService.sendEmail(emailParams)).rejects.toThrow();
    });
  });

  describe('manageDraft', () => {
    it('should create draft', async () => {
      (mockGmailClient.users.drafts.create as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: {
            id: 'draft1',
            message: { id: 'msg1' }
          }
        })
      );

      const result = await gmailService.manageDraft({
        action: 'create',
        email: testEmail,
        data: {
          to: ['[email protected]'],
          subject: 'Draft',
          body: 'Content'
        }
      }) as DraftResponse;

      expect(result).toHaveProperty('id', 'draft1');
    });

    it('should list drafts', async () => {
      // Mock the list call to return draft IDs
      (mockGmailClient.users.drafts.list as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: {
            drafts: [
              { id: 'draft1' },
              { id: 'draft2' }
            ],
            resultSizeEstimate: 2
          }
        })
      );

      // Mock the get call for each draft
      (mockGmailClient.users.drafts.get as jest.Mock)
        .mockImplementationOnce(() => Promise.resolve({
          data: {
            id: 'draft1',
            message: {
              id: 'msg1',
              threadId: 'thread1',
              labelIds: ['DRAFT']
            }
          }
        }))
        .mockImplementationOnce(() => Promise.resolve({
          data: {
            id: 'draft2',
            message: {
              id: 'msg2',
              threadId: 'thread2',
              labelIds: ['DRAFT']
            }
          }
        }));

      const result = await gmailService.manageDraft({
        action: 'read',
        email: testEmail
      }) as GetDraftsResponse;

      expect(result).toHaveProperty('drafts');
      expect(result.drafts.length).toBe(2);
      expect(result.drafts[0]).toHaveProperty('id', 'draft1');
      expect(result.drafts[1]).toHaveProperty('id', 'draft2');
    });

    it('should send draft', async () => {
      (mockGmailClient.users.drafts.send as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: {
            id: 'msg1',
            threadId: 'thread1',
            labelIds: ['SENT']
          }
        })
      );

      const result = await gmailService.manageDraft({
        action: 'send',
        email: testEmail,
        draftId: 'draft1'
      }) as SendEmailResponse;

      expect(result).toHaveProperty('messageId', 'msg1');
      expect(result).toHaveProperty('labelIds');
    });
  });

  describe('getWorkspaceGmailSettings', () => {
    it('should get settings', async () => {
      (mockGmailClient.users.getProfile as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: {
            emailAddress: testEmail,
            messagesTotal: 100
          }
        })
      );

      const result = await gmailService.getWorkspaceGmailSettings({
        email: testEmail
      });

      expect(result.profile.emailAddress).toBe(testEmail);
      expect(result.settings).toBeDefined();
    });

    it('should handle settings fetch error', async () => {
      (mockGmailClient.users.getProfile as jest.Mock).mockImplementation(() =>
        Promise.reject(new Error('Failed to fetch'))
      );

      await expect(gmailService.getWorkspaceGmailSettings({
        email: testEmail
      })).rejects.toThrow();
    });
  });
});

```

--------------------------------------------------------------------------------
/src/modules/tools/registry.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";

export interface ToolMetadata extends Tool {
  category: string;
  aliases?: string[];
}

export interface ToolCategory {
  name: string;
  description: string;
  tools: ToolMetadata[];
}

export class ToolRegistry {
  private tools: Map<string, ToolMetadata> = new Map();
  private categories: Map<string, ToolCategory> = new Map();
  private aliasMap: Map<string, string> = new Map();

  constructor(tools: ToolMetadata[]) {
    this.registerTools(tools);
  }

  private registerTools(tools: ToolMetadata[]): void {
    for (const tool of tools) {
      // Register the main tool
      this.tools.set(tool.name, tool);

      // Register category
      if (!this.categories.has(tool.category)) {
        this.categories.set(tool.category, {
          name: tool.category,
          description: '', // Could be added in future
          tools: []
        });
      }
      this.categories.get(tool.category)?.tools.push(tool);

      // Register aliases
      if (tool.aliases) {
        for (const alias of tool.aliases) {
          this.aliasMap.set(alias, tool.name);
        }
      }
    }
  }

  getTool(name: string): ToolMetadata | undefined {
    // Try direct lookup
    const tool = this.tools.get(name);
    if (tool) {
      return tool;
    }

    // Try alias lookup
    const mainName = this.aliasMap.get(name);
    if (mainName) {
      return this.tools.get(mainName);
    }

    return undefined;
  }

  getAllTools(): ToolMetadata[] {
    return Array.from(this.tools.values());
  }

  getCategories(): ToolCategory[] {
    return Array.from(this.categories.values());
  }

  private calculateLevenshteinDistance(a: string, b: string): number {
    const matrix: number[][] = [];

    // Initialize matrix
    for (let i = 0; i <= b.length; i++) {
      matrix[i] = [i];
    }
    for (let j = 0; j <= a.length; j++) {
      matrix[0][j] = j;
    }

    // Fill matrix
    for (let i = 1; i <= b.length; i++) {
      for (let j = 1; j <= a.length; j++) {
        if (b.charAt(i - 1) === a.charAt(j - 1)) {
          matrix[i][j] = matrix[i - 1][j - 1];
        } else {
          matrix[i][j] = Math.min(
            matrix[i - 1][j - 1] + 1, // substitution
            matrix[i][j - 1] + 1,     // insertion
            matrix[i - 1][j] + 1      // deletion
          );
        }
      }
    }

    return matrix[b.length][a.length];
  }

  private tokenize(name: string): string[] {
    return name.toLowerCase().split(/[_\s]+/);
  }

  private calculateSimilarityScore(searchTokens: string[], targetTokens: string[]): number {
    // First try exact token matching with position awareness
    const searchStr = searchTokens.join('_');
    const targetStr = targetTokens.join('_');
    
    // Perfect match
    if (searchStr === targetStr) {
      return 1.0;
    }
    
    // Check if tokens are the same but in different order
    const searchSet = new Set(searchTokens);
    const targetSet = new Set(targetTokens);
    if (searchSet.size === targetSet.size && 
        [...searchSet].every(token => targetSet.has(token))) {
      return 0.9;
    }

    // Calculate token-by-token similarity
    let score = 0;
    const usedTargetTokens = new Set<number>();
    let matchedTokens = 0;

    for (const searchToken of searchTokens) {
      let bestTokenScore = 0;
      let bestTokenIndex = -1;

      targetTokens.forEach((targetToken, index) => {
        if (usedTargetTokens.has(index)) return;

        // Exact match gets highest score
        if (searchToken === targetToken) {
          const positionPenalty = Math.abs(searchTokens.indexOf(searchToken) - index) * 0.1;
          const tokenScore = Math.max(0.8, 1.0 - positionPenalty);
          if (tokenScore > bestTokenScore) {
            bestTokenScore = tokenScore;
            bestTokenIndex = index;
          }
          return;
        }

        // Substring match gets good score
        if (targetToken.includes(searchToken) || searchToken.includes(targetToken)) {
          const tokenScore = 0.7;
          if (tokenScore > bestTokenScore) {
            bestTokenScore = tokenScore;
            bestTokenIndex = index;
          }
          return;
        }

        // Levenshtein distance for fuzzy matching
        const distance = this.calculateLevenshteinDistance(searchToken, targetToken);
        const maxLength = Math.max(searchToken.length, targetToken.length);
        const tokenScore = 1 - (distance / maxLength);
        
        if (tokenScore > 0.6 && tokenScore > bestTokenScore) {
          bestTokenScore = tokenScore;
          bestTokenIndex = index;
        }
      });

      if (bestTokenIndex !== -1) {
        score += bestTokenScore;
        usedTargetTokens.add(bestTokenIndex);
        matchedTokens++;
      }
    }

    // Penalize if not all tokens were matched
    const matchRatio = matchedTokens / searchTokens.length;
    const finalScore = (score / searchTokens.length) * matchRatio;

    // Additional penalty for length mismatch
    const lengthPenalty = Math.abs(searchTokens.length - targetTokens.length) * 0.1;
    return Math.max(0, finalScore - lengthPenalty);
  }

  private isCommonTypo(a: string, b: string): boolean {
    const commonTypos: { [key: string]: string[] } = {
      'label': ['lable', 'labl', 'lbl'],
      'email': ['emil', 'mail', 'emal'],
      'calendar': ['calender', 'calander', 'caldr'],
      'workspace': ['workspce', 'wrkspace', 'wrkspc'],
      'create': ['creat', 'crete', 'craete'],
      'message': ['mesage', 'msg', 'messge'],
      'draft': ['draf', 'drft', 'darft']
    };

    // Check both directions (a->b and b->a)
    for (const [word, typos] of Object.entries(commonTypos)) {
      if ((a === word && typos.includes(b)) || (b === word && typos.includes(a))) {
        return true;
      }
    }
    return false;
  }

  findSimilarTools(name: string, maxSuggestions: number = 3): ToolMetadata[] {
    const searchTokens = this.tokenize(name);
    const matches: Array<{ tool: ToolMetadata; score: number }> = [];

    for (const tool of this.getAllTools()) {
      let bestScore = 0;

      // Check main tool name
      const nameTokens = this.tokenize(tool.name);
      bestScore = this.calculateSimilarityScore(searchTokens, nameTokens);

      // Check for common typos in each token
      const hasCommonTypo = searchTokens.some(searchToken =>
        nameTokens.some(nameToken => this.isCommonTypo(searchToken, nameToken))
      );
      if (hasCommonTypo) {
        bestScore = Math.max(bestScore, 0.8); // Boost score for common typos
      }

      // Check aliases
      if (tool.aliases) {
        for (const alias of tool.aliases) {
          const aliasTokens = this.tokenize(alias);
          const aliasScore = this.calculateSimilarityScore(searchTokens, aliasTokens);
          
          // Check for common typos in aliases too
          if (searchTokens.some(searchToken =>
              aliasTokens.some(aliasToken => this.isCommonTypo(searchToken, aliasToken)))) {
            bestScore = Math.max(bestScore, 0.8);
          }
          
          bestScore = Math.max(bestScore, aliasScore);
        }
      }

      // More lenient threshold (0.4 instead of 0.5) and include common typos
      if (bestScore > 0.4 || hasCommonTypo) {
        matches.push({ tool, score: bestScore });
      }
    }

    // Sort by score (highest first) and return top matches
    return matches
      .sort((a, b) => b.score - a.score)
      .slice(0, maxSuggestions)
      .map(m => m.tool);
  }

  formatErrorWithSuggestions(invalidToolName: string): string {
    const similarTools = this.findSimilarTools(invalidToolName);
    const categories = this.getCategories();

    let message = `Tool '${invalidToolName}' not found.\n\n`;

    if (similarTools.length > 0) {
      message += 'Did you mean:\n';
      for (const tool of similarTools) {
        message += `- ${tool.name} (${tool.category})\n`;
        if (tool.aliases && tool.aliases.length > 0) {
          message += `  Aliases: ${tool.aliases.join(', ')}\n`;
        }
      }
      message += '\n';
    }

    message += 'Available categories:\n';
    for (const category of categories) {
      const toolNames = category.tools.map(t => t.name.replace('workspace_', '')).join(', ');
      message += `- ${category.name}: ${toolNames}\n`;
    }

    return message;
  }

  // Helper method to get all available tool names including aliases
  getAllToolNames(): string[] {
    const names: string[] = [];
    for (const tool of this.tools.values()) {
      names.push(tool.name);
      if (tool.aliases) {
        names.push(...tool.aliases);
      }
    }
    return names;
  }
}

```

--------------------------------------------------------------------------------
/src/modules/gmail/__tests__/label.test.ts:
--------------------------------------------------------------------------------

```typescript
import { GmailService } from '../services/base.js';
import { gmail_v1 } from 'googleapis';
import { Label } from '../types.js';
import { getAccountManager } from '../../../modules/accounts/index.js';
import { AccountManager } from '../../../modules/accounts/manager.js';
import logger from '../../../utils/logger.js';

jest.mock('../../../modules/accounts/index.js');
jest.mock('../../../modules/accounts/manager.js');
jest.mock('../../../utils/logger.js', () => ({
  default: {
    error: jest.fn(),
    warn: jest.fn(),
    info: jest.fn(),
    debug: jest.fn()
  }
}));

describe('Gmail Label Service', () => {
  let gmailService: GmailService;
  let mockClient: any;
  const testEmail = '[email protected]';

  beforeAll(() => {
    // Mock getAccountManager at module level
    (getAccountManager as jest.Mock).mockReturnValue({
      validateToken: jest.fn().mockResolvedValue({ valid: true, token: {} }),
      getAuthClient: jest.fn().mockResolvedValue({})
    });
  });

  beforeEach(async () => {
    
    // Create a fresh instance for each test
    gmailService = new GmailService();

    // Create mock client
    mockClient = {
      users: {
        labels: {
          list: jest.fn(),
          create: jest.fn(),
          patch: jest.fn(),
          delete: jest.fn()
        },
        messages: {
          modify: jest.fn()
        }
      }
    };

    // Mock the Gmail client methods at service level
    (gmailService as any).getGmailClient = jest.fn().mockResolvedValue(mockClient);

    // Initialize the service and update label service client
    await gmailService.initialize();
    (gmailService as any).labelService.updateClient(mockClient);
  });

  describe('manageLabel - read', () => {
    it('should fetch all labels', async () => {
      // Simple mock response
      const mockResponse = {
        data: {
          labels: [
            {
              id: 'label1',
              name: 'Test Label',
              type: 'user',
              messageListVisibility: 'show',
              labelListVisibility: 'labelShow'
            }
          ]
        }
      };

      // Set up mock
      (mockClient.users.labels.list as jest.Mock).mockResolvedValue(mockResponse);

      const result = await gmailService.manageLabel({
        action: 'read',
        email: testEmail
      });

      // Simple assertions
      expect((result as any).labels).toHaveLength(1);
      expect((result as any).labels[0].id).toBe('label1');
      expect((result as any).labels[0].name).toBe('Test Label');
      expect(mockClient.users.labels.list).toHaveBeenCalledWith({
        userId: testEmail
      });
    });

    it('should handle empty labels response', async () => {
      // Simple mock for empty response
      (mockClient.users.labels.list as jest.Mock).mockResolvedValue({
        data: { labels: [] }
      });

      const result = await gmailService.manageLabel({
        action: 'read',
        email: testEmail
      });
      expect((result as any).labels).toHaveLength(0);
    });

    it('should handle errors when fetching labels', async () => {
      // Simple error mock
      (mockClient.users.labels.list as jest.Mock).mockRejectedValue(new Error('API Error'));

      await expect(gmailService.manageLabel({
        action: 'read',
        email: testEmail
      }))
        .rejects
        .toThrow('Failed to fetch labels');
    });
  });

  describe('manageLabel - create', () => {
    it('should create a new label', async () => {
      // Simple mock response
      const mockResponse = {
        data: {
          id: 'label1',
          name: 'Test Label',
          type: 'user',
          messageListVisibility: 'show',
          labelListVisibility: 'labelShow'
        }
      };

      // Set up mock
      (mockClient.users.labels.create as jest.Mock).mockResolvedValue(mockResponse);

      const result = await gmailService.manageLabel({
        action: 'create',
        email: testEmail,
        data: {
          name: 'Test Label'
        }
      });

      // Simple assertions
      expect((result as Label).id).toBe('label1');
      expect((result as Label).name).toBe('Test Label');
      expect(mockClient.users.labels.create).toHaveBeenCalledWith({
        userId: testEmail,
        requestBody: expect.objectContaining({
          name: 'Test Label'
        })
      });
    });

    it('should handle errors when creating a label', async () => {
      // Simple error mock
      (mockClient.users.labels.create as jest.Mock).mockRejectedValue(new Error('API Error'));

      await expect(gmailService.manageLabel({
        action: 'create',
        email: testEmail,
        data: {
          name: 'Test Label'
        }
      })).rejects.toThrow('Failed to create label');
    });
  });

  describe('manageLabel - update', () => {
    it('should update an existing label', async () => {
      // Simple mock response
      const mockResponse = {
        data: {
          id: 'label1',
          name: 'Updated Label',
          type: 'user',
          messageListVisibility: 'show',
          labelListVisibility: 'labelShow'
        }
      };

      // Set up mock
      (mockClient.users.labels.patch as jest.Mock).mockResolvedValue(mockResponse);

      const result = await gmailService.manageLabel({
        action: 'update',
        email: testEmail,
        labelId: 'label1',
        data: {
          name: 'Updated Label'
        }
      });

      // Simple assertions
      expect((result as Label).id).toBe('label1');
      expect((result as Label).name).toBe('Updated Label');
      expect(mockClient.users.labels.patch).toHaveBeenCalledWith({
        userId: testEmail,
        id: 'label1',
        requestBody: expect.objectContaining({
          name: 'Updated Label'
        })
      });
    });

    it('should handle errors when updating a label', async () => {
      // Simple error mock
      (mockClient.users.labels.patch as jest.Mock).mockRejectedValue(new Error('API Error'));

      await expect(gmailService.manageLabel({
        action: 'update',
        email: testEmail,
        labelId: 'label1',
        data: {
          name: 'Updated Label'
        }
      })).rejects.toThrow('Failed to update label');
    });
  });

  describe('manageLabel - delete', () => {
    it('should delete a label', async () => {
      // Simple mock response
      (mockClient.users.labels.delete as jest.Mock).mockResolvedValue({});

      // Execute and verify
      await gmailService.manageLabel({
        action: 'delete',
        email: testEmail,
        labelId: 'label1'
      });

      // Simple assertions
      expect(mockClient.users.labels.delete).toHaveBeenCalledWith({
        userId: testEmail,
        id: 'label1'
      });
    });

    it('should handle errors when deleting a label', async () => {
      // Simple error mock
      (mockClient.users.labels.delete as jest.Mock).mockRejectedValue(new Error('API Error'));

      await expect(gmailService.manageLabel({
        action: 'delete',
        email: testEmail,
        labelId: 'label1'
      })).rejects.toThrow('Failed to delete label');
    });
  });

  describe('manageLabelAssignment', () => {
    it('should add labels to a message', async () => {
      // Simple mock response
      (mockClient.users.messages.modify as jest.Mock).mockResolvedValue({});

      // Execute and verify
      await gmailService.manageLabelAssignment({
        action: 'add',
        email: testEmail,
        messageId: 'msg1',
        labelIds: ['label1']
      });

      // Simple assertions
      expect(mockClient.users.messages.modify).toHaveBeenCalledWith({
        userId: testEmail,
        id: 'msg1',
        requestBody: {
          addLabelIds: ['label1'],
          removeLabelIds: []
        }
      });
    });

    it('should remove labels from a message', async () => {
      // Simple mock response
      (mockClient.users.messages.modify as jest.Mock).mockResolvedValue({});

      // Execute and verify
      await gmailService.manageLabelAssignment({
        action: 'remove',
        email: testEmail,
        messageId: 'msg1',
        labelIds: ['label2']
      });

      // Simple assertions
      expect(mockClient.users.messages.modify).toHaveBeenCalledWith({
        userId: testEmail,
        id: 'msg1',
        requestBody: {
          addLabelIds: [],
          removeLabelIds: ['label2']
        }
      });
    });

    it('should handle errors when modifying message labels', async () => {
      // Simple error mock
      (mockClient.users.messages.modify as jest.Mock).mockRejectedValue(new Error('API Error'));

      await expect(gmailService.manageLabelAssignment({
        action: 'add',
        email: testEmail,
        messageId: 'msg1',
        labelIds: ['label1']
      })).rejects.toThrow('Failed to modify message labels');
    });
  });
});

```

--------------------------------------------------------------------------------
/src/modules/accounts/token.ts:
--------------------------------------------------------------------------------

```typescript
import fs from 'fs/promises';
import path from 'path';
import { AccountError, TokenStatus, TokenRenewalResult } from './types.js';
import { GoogleOAuthClient } from './oauth.js';
import logger from '../../utils/logger.js';

/**
 * Manages OAuth token operations.
 * Focuses on basic token storage, retrieval, and refresh.
 * Auth issues are handled via 401 responses rather than pre-validation.
 */
export class TokenManager {
  private readonly credentialsPath: string;
  private oauthClient?: GoogleOAuthClient;
  private readonly TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; // 5 minutes buffer

  constructor(oauthClient?: GoogleOAuthClient) {
    // Use environment variable or config, fallback to Docker default
    const defaultPath = process.env.CREDENTIALS_PATH || 
                       (process.env.MCP_MODE ? path.resolve(process.env.HOME || '', '.mcp/google-workspace-mcp/credentials') : '/app/config/credentials');
    this.credentialsPath = defaultPath;
    this.oauthClient = oauthClient;
  }

  setOAuthClient(client: GoogleOAuthClient) {
    this.oauthClient = client;
  }

  private getTokenPath(email: string): string {
    const sanitizedEmail = email.replace(/[^a-zA-Z0-9]/g, '-');
    return path.join(this.credentialsPath, `${sanitizedEmail}.token.json`);
  }

  async saveToken(email: string, tokenData: any): Promise<void> {
    logger.info(`Saving token for account: ${email}`);
    try {
      // Ensure base credentials directory exists
      await fs.mkdir(this.credentialsPath, { recursive: true });
      const tokenPath = this.getTokenPath(email);
      await fs.writeFile(tokenPath, JSON.stringify(tokenData, null, 2));
      logger.debug(`Token saved successfully at: ${tokenPath}`);
    } catch (error) {
      throw new AccountError(
        'Failed to save token',
        'TOKEN_SAVE_ERROR',
        'Please ensure the credentials directory is writable'
      );
    }
  }

  async loadToken(email: string): Promise<any> {
    logger.debug(`Loading token for account: ${email}`);
    try {
      const tokenPath = this.getTokenPath(email);
      const data = await fs.readFile(tokenPath, 'utf-8');
      return JSON.parse(data);
    } catch (error) {
      if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
        // File doesn't exist - return null to trigger OAuth flow
        return null;
      }
      throw new AccountError(
        'Failed to load token',
        'TOKEN_LOAD_ERROR',
        'Please ensure the token file exists and is readable'
      );
    }
  }

  async deleteToken(email: string): Promise<void> {
    logger.info(`Deleting token for account: ${email}`);
    try {
      const tokenPath = this.getTokenPath(email);
      await fs.unlink(tokenPath);
      logger.debug('Token file deleted successfully');
    } catch (error) {
      if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
        throw new AccountError(
          'Failed to delete token',
          'TOKEN_DELETE_ERROR',
          'Please ensure you have permission to delete the token file'
        );
      }
    }
  }

  /**
   * Basic token validation - just checks if token exists and isn't expired.
   * No scope validation - auth issues handled via 401 responses.
   */
  /**
   * Attempts to automatically renew a token if it's expired or near expiry
   * Returns the renewal result and new token if successful
   */
  async autoRenewToken(email: string): Promise<TokenRenewalResult> {
    logger.debug(`Attempting auto-renewal for account: ${email}`);
    
    try {
      const token = await this.loadToken(email);
      
      if (!token) {
        return {
          success: false,
          status: 'NO_TOKEN',
          reason: 'No token found'
        };
      }

      if (!token.expiry_date) {
        return {
          success: false,
          status: 'INVALID',
          reason: 'Invalid token format'
        };
      }

      // Check if token is expired or will expire soon
      const now = Date.now();
      if (token.expiry_date <= now + this.TOKEN_EXPIRY_BUFFER_MS) {
        if (!token.refresh_token || !this.oauthClient) {
          return {
            success: false,
            status: 'REFRESH_FAILED',
            reason: 'No refresh token or OAuth client available'
          };
        }

        try {
          // Attempt to refresh the token
          const newToken = await this.oauthClient.refreshToken(token.refresh_token);
          await this.saveToken(email, newToken);
          logger.info('Token refreshed successfully');
          return {
            success: true,
            status: 'REFRESHED',
            token: newToken
          };
        } catch (error) {
          // Check if the error indicates an invalid/revoked refresh token
          const errorMessage = error instanceof Error ? error.message.toLowerCase() : '';
          const isRefreshTokenInvalid = 
            errorMessage.includes('invalid_grant') || 
            errorMessage.includes('token has been revoked') ||
            errorMessage.includes('token not found');

          if (!isRefreshTokenInvalid) {
            // If it's not a refresh token issue, try one more time
            try {
              logger.warn('First refresh attempt failed, trying once more');
              const newToken = await this.oauthClient.refreshToken(token.refresh_token);
              await this.saveToken(email, newToken);
              logger.info('Token refreshed successfully on second attempt');
              return {
                success: true,
                status: 'REFRESHED',
                token: newToken
              };
            } catch (secondError) {
              logger.error('Both refresh attempts failed, but refresh token may still be valid');
              return {
                success: false,
                status: 'REFRESH_FAILED',
                reason: 'Token refresh failed, temporary error',
                canRetry: true
              };
            }
          }

          // Refresh token is invalid, need full reauth
          logger.error('Refresh token is invalid or revoked');
          return {
            success: false,
            status: 'REFRESH_FAILED',
            reason: 'Refresh token is invalid or revoked',
            canRetry: false
          };
        }
      }

      // Token is still valid
      return {
        success: true,
        status: 'VALID',
        token
      };
    } catch (error) {
      logger.error('Token auto-renewal error', error as Error);
      return {
        success: false,
        status: 'ERROR',
        reason: 'Token auto-renewal failed'
      };
    }
  }

  async validateToken(email: string, skipValidationForNew: boolean = false): Promise<TokenStatus> {
    logger.debug(`Validating token for account: ${email}`);
    
    try {
      const token = await this.loadToken(email);
      
      if (!token) {
        logger.debug('No token found');
        return {
          valid: false,
          status: 'NO_TOKEN',
          reason: 'No token found'
        };
      }

      // Skip validation if this is a new account setup
      if (skipValidationForNew) {
        logger.debug('Skipping validation for new account setup');
        return {
          valid: true,
          status: 'VALID',
          token
        };
      }

      if (!token.expiry_date) {
        logger.debug('Token missing expiry date');
        return {
          valid: false,
          status: 'INVALID',
          reason: 'Invalid token format'
        };
      }

      if (token.expiry_date < Date.now()) {
        logger.debug('Token has expired, attempting refresh');
        if (token.refresh_token && this.oauthClient) {
          try {
            const newToken = await this.oauthClient.refreshToken(token.refresh_token);
            await this.saveToken(email, newToken);
            logger.info('Token refreshed successfully');
      return {
        valid: true,
        status: 'REFRESHED',
        token: newToken,
        requiredScopes: newToken.scope ? newToken.scope.split(' ') : undefined
      };
          } catch (error) {
            logger.error('Token refresh failed', error as Error);
            return {
              valid: false,
              status: 'REFRESH_FAILED',
              reason: 'Token refresh failed'
            };
          }
        }
        logger.debug('No refresh token available');
        return {
          valid: false,
          status: 'EXPIRED',
          reason: 'Token expired and no refresh token available'
        };
      }

      logger.debug('Token is valid');
      return {
        valid: true,
        status: 'VALID',
        token,
        requiredScopes: token.scope ? token.scope.split(' ') : undefined
      };
    } catch (error) {
      logger.error('Token validation error', error as Error);
      return {
        valid: false,
        status: 'ERROR',
        reason: 'Token validation failed'
      };
    }
  }
}

```

--------------------------------------------------------------------------------
/src/__tests__/modules/accounts/manager.test.ts:
--------------------------------------------------------------------------------

```typescript
import { AccountManager } from '../../../modules/accounts/manager.js';
import { mockAccounts, mockTokens } from '../../../__fixtures__/accounts.js';

// Simple mocks for token and oauth
jest.mock('../../../modules/accounts/token.js', () => ({
  TokenManager: jest.fn().mockImplementation(() => ({
    validateToken: jest.fn().mockResolvedValue({
      valid: true,
      status: 'VALID',
      token: { access_token: 'test-token' }
    }),
    saveToken: jest.fn().mockResolvedValue(undefined),
    deleteToken: jest.fn().mockResolvedValue(undefined),
    autoRenewToken: jest.fn().mockResolvedValue({
      success: true,
      status: 'VALID',
      token: { access_token: 'test-token' }
    })
  }))
}));

jest.mock('../../../modules/accounts/oauth.js', () => ({
  GoogleOAuthClient: jest.fn().mockImplementation(() => ({
    ensureInitialized: jest.fn().mockResolvedValue(undefined),
    getTokenFromCode: jest.fn().mockResolvedValue(mockTokens.valid),
    refreshToken: jest.fn().mockResolvedValue(mockTokens.valid),
    generateAuthUrl: jest.fn().mockReturnValue('https://mock-auth-url'),
    getAuthClient: jest.fn().mockReturnValue({
      setCredentials: jest.fn(),
      getAccessToken: jest.fn().mockResolvedValue({ token: 'test-token' })
    })
  }))
}));

// Simple file system mock
jest.mock('fs/promises', () => ({
  readFile: jest.fn(),
  writeFile: jest.fn().mockResolvedValue(undefined),
  mkdir: jest.fn().mockResolvedValue(undefined)
}));

jest.mock('path', () => ({
  resolve: jest.fn().mockReturnValue('/mock/accounts.json'),
  dirname: jest.fn().mockReturnValue('/mock')
}));

describe('AccountManager', () => {
  let accountManager: AccountManager;
  const fs = require('fs/promises');

  beforeEach(() => {
    jest.clearAllMocks();
    jest.resetModules();
    // Clear environment variables that affect path resolution
    delete process.env.MCP_MODE;
    delete process.env.HOME;
    process.env.ACCOUNTS_PATH = '/mock/accounts.json';
    // Default successful file read
    fs.readFile.mockResolvedValue(JSON.stringify(mockAccounts));
    
    // Reset TokenManager mock to default implementation
    const TokenManager = require('../../../modules/accounts/token.js').TokenManager;
    TokenManager.mockImplementation(() => ({
      validateToken: jest.fn().mockResolvedValue({ valid: true }),
      saveToken: jest.fn().mockResolvedValue(undefined),
      deleteToken: jest.fn().mockResolvedValue(undefined),
      autoRenewToken: jest.fn().mockResolvedValue({
        success: true,
        status: 'VALID',
        token: { access_token: 'test-token' }
      })
    }));
    
    accountManager = new AccountManager();
  });

  // Basic account operations
  describe('account operations', () => {
    it('should load accounts from file', async () => {
      await accountManager.initialize();
      const accounts = await accountManager.listAccounts();
      
      expect(accounts).toHaveLength(mockAccounts.accounts.length);
      expect(accounts[0].email).toBe(mockAccounts.accounts[0].email);
    });

    it('should add new account', async () => {
      await accountManager.initialize();
      const newAccount = await accountManager.addAccount(
        '[email protected]',
        'work',
        'New Test Account'
      );

      expect(newAccount.email).toBe('[email protected]');
      expect(fs.writeFile).toHaveBeenCalled();
    });

    it('should not add duplicate account', async () => {
      await accountManager.initialize();
      await expect(accountManager.addAccount(
        mockAccounts.accounts[0].email,
        'work',
        'Duplicate'
      )).rejects.toThrow('Account already exists');
    });
  });

  // File system operations
  describe('file operations', () => {
    it('should handle missing accounts file', async () => {
      // Reset all mocks and modules
      jest.clearAllMocks();
      jest.resetModules();
      
      // Re-require fs after reset
      const fs = require('fs/promises');
      
      // Setup error for first read attempt
      const error = new Error('File not found');
      (error as any).code = 'ENOENT';
      fs.readFile.mockRejectedValueOnce(error);
      
      // Mock path module
      jest.doMock('path', () => ({
        resolve: jest.fn().mockReturnValue('/mock/accounts.json'),
        dirname: jest.fn().mockReturnValue('/mock')
      }));
      
      // Re-require AccountManager to use fresh mocks
      const { AccountManager } = require('../../../modules/accounts/manager.js');
      accountManager = new AccountManager();
      
      // Initialize with empty file system
      await accountManager.initialize();
      
      // Mock empty response for listAccounts call
      fs.readFile.mockResolvedValueOnce('{"accounts":[]}');
      const accounts = await accountManager.listAccounts();
      
      // Verify results
      expect(accounts).toHaveLength(0);
      
      // Verify write was called with correct data
      expect(fs.writeFile).toHaveBeenCalledTimes(1);
      const [path, content] = fs.writeFile.mock.calls[0];
      expect(path).toBe('/mock/accounts.json');
      
      // Parse and re-stringify to normalize formatting
      const parsedContent = JSON.parse(content);
      expect(parsedContent).toEqual({ accounts: [] });
    });

    it('should handle invalid JSON', async () => {
      fs.readFile.mockResolvedValueOnce('invalid json');
      
      await expect(accountManager.initialize())
        .rejects
        .toThrow('Failed to parse accounts configuration');
    });
  });

  // Token validation (simplified)
  describe('token validation', () => {
    const testEmail = mockAccounts.accounts[0].email;

    it('should validate token successfully', async () => {
      await accountManager.initialize();
      const account = await accountManager.validateAccount(testEmail);
      
      expect(account.email).toBe(testEmail);
      expect(account.auth_status).toEqual({
        valid: true,
        status: 'VALID'
      });
    });

    it('should handle token validation failure', async () => {
      // Reset all mocks and modules
      jest.clearAllMocks();
      jest.resetModules();
      
      // Re-require fs and setup fresh state
      const fs = require('fs/promises');
      fs.readFile.mockResolvedValue(JSON.stringify(mockAccounts));
      
      // Create simple mock implementation
      const mockValidateToken = jest.fn().mockResolvedValue({ 
        valid: false,
        status: 'EXPIRED',
        reason: 'Token expired'
      });
      
      // Setup TokenManager with tracked mock
      jest.doMock('../../../modules/accounts/token.js', () => ({
        TokenManager: jest.fn().mockImplementation(() => ({
          validateToken: mockValidateToken,
          saveToken: jest.fn(),
          deleteToken: jest.fn()
        }))
      }));
      
      // Re-require AccountManager to use new mocks
      const { AccountManager } = require('../../../modules/accounts/manager.js');
      accountManager = new AccountManager();
      await accountManager.initialize();
      
      const account = await accountManager.validateAccount(testEmail);
      
      expect(mockValidateToken).toHaveBeenCalledWith(testEmail, false);
      expect(account.auth_status).toMatchObject({
        valid: false,
        status: 'EXPIRED',
        reason: 'Token expired'
      });
      expect(account.auth_status).toHaveProperty('authUrl');
    });
  });

  // OAuth operations (simplified)
  describe('oauth operations', () => {
    it('should handle token from auth code', async () => {
      await accountManager.initialize();
      const token = await accountManager.getTokenFromCode('test-code');
      
      expect(token).toEqual(mockTokens.valid);
    });

    it('should save token for account', async () => {
      // Reset all mocks and modules
      jest.clearAllMocks();
      jest.resetModules();
      
      // Re-require fs and setup fresh state
      const fs = require('fs/promises');
      fs.readFile.mockResolvedValue(JSON.stringify(mockAccounts));
      
      // Create mock implementation
      const mockSaveToken = jest.fn().mockResolvedValue(undefined);
      
      // Setup TokenManager with tracked mock
      jest.doMock('../../../modules/accounts/token.js', () => ({
        TokenManager: jest.fn().mockImplementation(() => ({
          validateToken: jest.fn().mockResolvedValue({ valid: true }),
          saveToken: mockSaveToken,
          deleteToken: jest.fn()
        }))
      }));
      
      // Re-require AccountManager to use new mocks
      const { AccountManager } = require('../../../modules/accounts/manager.js');
      accountManager = new AccountManager();
      await accountManager.initialize();
      
      const testEmail = '[email protected]';
      await accountManager.saveToken(testEmail, mockTokens.valid);
      
      expect(mockSaveToken).toHaveBeenCalledWith(testEmail, mockTokens.valid);
      expect(mockSaveToken).toHaveBeenCalledTimes(1);
    });
  });
});

```

--------------------------------------------------------------------------------
/llms-install.md:
--------------------------------------------------------------------------------

```markdown
# Google Workspace MCP Server - AI Assistant Installation Guide

This guide provides step-by-step instructions for setting up the Google Workspace MCP server with AI assistants like Claude Desktop and Cline.

## Overview

The Google Workspace MCP server enables AI assistants to interact with your Google Workspace services (Gmail, Calendar, Drive, Contacts) through a secure OAuth 2.0 authentication flow. The server runs in a Docker container and handles all API interactions.

## Prerequisites

### System Requirements
- Docker installed and running
- Internet connection for Google API access
- Available port 8080 for OAuth callback handling

### Google Cloud Setup

1. **Create Google Cloud Project**:
   - Visit [Google Cloud Console](https://console.cloud.google.com)
   - Create a new project or select an existing one
   - Note your project ID for reference

2. **Enable Required APIs**:
   Navigate to "APIs & Services" > "Library" and enable:
   - Gmail API
   - Google Calendar API
   - Google Drive API
   - People API (for Contacts)

3. **Configure OAuth Consent Screen**:
   - Go to "APIs & Services" > "OAuth consent screen"
   - Choose "External" user type
   - Fill in required application information:
     - App name: "Google Workspace MCP Server" (or your preference)
     - User support email: Your email address
     - Developer contact information: Your email address
   - Add yourself as a test user in the "Test users" section

4. **Create OAuth 2.0 Credentials**:
   - Go to "APIs & Services" > "Credentials"
   - Click "Create Credentials" > "OAuth 2.0 Client IDs"
   - **Important**: Select "Web application" (not Desktop application)
   - Set application name: "Google Workspace MCP Server"
   - Add authorized redirect URI: `http://localhost:8080`
   - Save and note your Client ID and Client Secret

## Installation Steps

### Step 1: Create Configuration Directory

Create a local directory for storing authentication tokens:

```bash
mkdir -p ~/.mcp/google-workspace-mcp
```

### Step 2: Configure Your MCP Client

Choose the appropriate configuration for your AI assistant:

#### For Claude Desktop

Edit your Claude Desktop configuration file:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
- **Linux**: `~/.config/Claude/claude_desktop_config.json`

Add the following configuration:

```json
{
  "mcpServers": {
    "google-workspace-mcp": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "-p", "8080:8080",
        "-v", "~/.mcp/google-workspace-mcp:/app/config",
        "-v", "~/Documents/workspace-mcp-files:/app/workspace",
        "-e", "GOOGLE_CLIENT_ID",
        "-e", "GOOGLE_CLIENT_SECRET",
        "-e", "LOG_MODE=strict",
        "ghcr.io/aaronsb/google-workspace-mcp:latest"
      ],
      "env": {
        "GOOGLE_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
        "GOOGLE_CLIENT_SECRET": "your-client-secret"
      }
    }
  }
}
```

#### For Cline (VS Code Extension)

Edit your Cline MCP settings file:
`~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`

```json
{
  "mcpServers": {
    "google-workspace-mcp": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "-p", "8080:8080",
        "-v", "~/.mcp/google-workspace-mcp:/app/config",
        "-v", "~/Documents/workspace-mcp-files:/app/workspace",
        "-e", "GOOGLE_CLIENT_ID",
        "-e", "GOOGLE_CLIENT_SECRET",
        "-e", "LOG_MODE=strict",
        "ghcr.io/aaronsb/google-workspace-mcp:latest"
      ],
      "env": {
        "GOOGLE_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
        "GOOGLE_CLIENT_SECRET": "your-client-secret"
      }
    }
  }
}
```

**Important Configuration Notes**:
- Replace `your-client-id.apps.googleusercontent.com` with your actual Google OAuth Client ID
- Replace `your-client-secret` with your actual Google OAuth Client Secret
- The `-p 8080:8080` port mapping is required for OAuth callback handling
- Adjust volume paths if you prefer different local directories

### Step 3: Restart Your AI Assistant

After updating the configuration:
- **Claude Desktop**: Completely quit and restart the application
- **Cline**: Restart VS Code or reload the Cline extension

## Authentication Process

### Initial Account Setup

1. **Start Authentication**:
   Ask your AI assistant: "Add my Google account" or "Set up Google Workspace access"

2. **OAuth Flow**:
   - The assistant will provide a Google authorization URL
   - Click the URL to open it in your browser
   - Sign in to your Google account
   - Review and accept the requested permissions
   - You'll be redirected to a success page showing your authorization code

3. **Complete Authentication**:
   - Copy the authorization code from the success page
   - Provide the code back to your AI assistant
   - The assistant will complete the authentication and save your tokens

### Managing Multiple Accounts

You can authenticate multiple Google accounts:
- Each account is stored separately with its own tokens
- Use account categorization (e.g., "work", "personal") for organization
- Switch between accounts as needed for different operations

## Verification

### Test Your Setup

After authentication, verify the setup works:

1. **List Accounts**: Ask "List my Google accounts" to see authenticated accounts
2. **Test Gmail**: Ask "Show me my recent emails"
3. **Test Calendar**: Ask "What's on my calendar today?"
4. **Test Drive**: Ask "List files in my Google Drive"

### Common Usage Examples

- "Search for emails from [email protected] in the last week"
- "Create a calendar event for tomorrow at 2 PM"
- "Upload this document to my Google Drive"
- "Show me my contacts with 'Smith' in the name"

## Troubleshooting

### Authentication Issues

**Problem**: "Invalid OAuth credentials" error
**Solution**:
- Verify Client ID and Client Secret are correctly copied
- Ensure OAuth consent screen is properly configured
- Check that you're added as a test user

**Problem**: "Connection refused" on localhost:8080
**Solution**:
- Verify port 8080 is not blocked by firewall
- Ensure Docker has permission to bind to port 8080
- Check that no other service is using port 8080

### Configuration Issues

**Problem**: MCP server not starting
**Solution**:
- Verify Docker is running

   macOS:
   - Shut down Docker fully from command line with `pkill -SIGHUP -f /Applications/Docker.app 'docker serve'`
   - Restart Docker Desktop
   - Restart your MCP client (Claude Desktop or Cursor/Cline/etc.)

   Windows:
   - Open Task Manager (Ctrl+Shift+Esc)
   - Find and end the "Docker Desktop" process
   - Restart Docker Desktop from the Start menu
   - Restart your MCP client (Claude Desktop or Cursor/Cline/etc.)

- Check that configuration directory exists and has proper permissions
- Ensure Docker image can be pulled from registry

**Problem**: "Directory not found" errors
**Solution**:
- Create the config directory: `mkdir -p ~/.mcp/google-workspace-mcp`
- Verify volume mount paths in configuration are correct
- Check file permissions on mounted directories

### API Issues

**Problem**: "API not enabled" errors
**Solution**:
- Verify all required APIs are enabled in Google Cloud Console
- Wait a few minutes after enabling APIs for changes to propagate
- Check API quotas and limits in Google Cloud Console

## Security Best Practices

1. **Credential Management**:
   - Store OAuth credentials securely in MCP configuration
   - Never commit credentials to version control
   - Regularly rotate OAuth client secrets

2. **Access Control**:
   - Use minimal required API scopes
   - Regularly review and audit account access
   - Remove unused accounts from authentication

3. **Network Security**:
   - OAuth callback server only runs during authentication
   - All API communication uses HTTPS
   - Tokens are stored locally and encrypted

## Advanced Configuration

### Custom File Workspace

To use a different directory for file operations:

```json
"args": [
  "run",
  "--rm",
  "-i",
  "-p", "8080:8080",
  "-v", "~/.mcp/google-workspace-mcp:/app/config",
  "-v", "/path/to/your/workspace:/app/workspace",
  "-e", "WORKSPACE_BASE_PATH=/app/workspace",
  "ghcr.io/aaronsb/google-workspace-mcp:latest"
]
```

### Logging Configuration

For debugging, you can adjust logging levels:

```json
"env": {
  "GOOGLE_CLIENT_ID": "your-client-id",
  "GOOGLE_CLIENT_SECRET": "your-client-secret",
  "LOG_MODE": "normal",
  "LOG_LEVEL": "debug"
}
```

## Getting Help

If you encounter issues:

1. Check the [main documentation](README.md) for additional troubleshooting
2. Review [error documentation](docs/ERRORS.md) for specific error codes
3. Examine Docker logs: `docker logs <container-id>`
4. Submit issues on the project's GitHub repository

## Next Steps

Once setup is complete:
- Explore the [API documentation](docs/API.md) for detailed tool usage
- Review [examples](docs/EXAMPLES.md) for common use cases
- Consider setting up multiple accounts for different workflows

```

--------------------------------------------------------------------------------
/src/modules/accounts/callback-server.ts:
--------------------------------------------------------------------------------

```typescript
import http from 'http';
import url from 'url';
import logger from '../../utils/logger.js';

export class OAuthCallbackServer {
  private static instance?: OAuthCallbackServer;
  private server?: http.Server;
  private port: number = 8080;
  private isRunning: boolean = false;
  private pendingPromises: Map<string, { resolve: (code: string) => void; reject: (error: Error) => void }> = new Map();
  private authHandler?: (code: string, state: string) => Promise<void>;
  
  private constructor() {}
  
  static getInstance(): OAuthCallbackServer {
    if (!OAuthCallbackServer.instance) {
      OAuthCallbackServer.instance = new OAuthCallbackServer();
    }
    return OAuthCallbackServer.instance;
  }
  
  async ensureServerRunning(): Promise<void> {
    if (this.isRunning) {
      return;
    }
    
    return new Promise((resolve, reject) => {
      this.server = http.createServer((req, res) => {
        const parsedUrl = url.parse(req.url || '', true);
        
        // Handle the auto-complete endpoint
        if (parsedUrl.pathname === '/complete-auth' && req.method === 'POST') {
          let body = '';
          req.on('data', chunk => {
            body += chunk.toString();
          });
          req.on('end', async () => {
            try {
              const { code, state } = JSON.parse(body);
              
              // Automatically complete the authentication
              if (this.authHandler) {
                await this.authHandler(code, state || 'default');
                logger.info('OAuth authentication completed automatically');
              }
              
              // Also resolve any pending promises (for backward compatibility)
              const pending = this.pendingPromises.get(state || 'default');
              if (pending) {
                pending.resolve(code);
                this.pendingPromises.delete(state || 'default');
              }
              
              res.writeHead(200, { 'Content-Type': 'application/json' });
              res.end(JSON.stringify({ success: true }));
            } catch (error) {
              logger.error('Failed to process auto-complete request:', error);
              res.writeHead(400, { 'Content-Type': 'application/json' });
              res.end(JSON.stringify({ success: false, error: 'Invalid request' }));
            }
          });
          return;
        }
        
        if (parsedUrl.pathname === '/') {
          const code = parsedUrl.query.code as string;
          const error = parsedUrl.query.error as string;
          const state = parsedUrl.query.state as string || 'default';
          
          if (error) {
            res.writeHead(400, { 'Content-Type': 'text/html' });
            res.end(`
              <html>
                <body>
                  <h1>Authorization Failed</h1>
                  <p>Error: ${error}</p>
                  <p>You can close this window.</p>
                </body>
              </html>
            `);
            
            // Reject any pending promises
            const pending = this.pendingPromises.get(state);
            if (pending) {
              pending.reject(new Error(`OAuth error: ${error}`));
              this.pendingPromises.delete(state);
            }
            return;
          }
          
          if (code) {
            res.writeHead(200, { 'Content-Type': 'text/html' });
            res.end(`
              <html>
                <head>
                  <title>Google OAuth Authorization Successful</title>
                  <style>
                    body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
                    .success-message { 
                      background: #4CAF50; 
                      color: white; 
                      padding: 20px; 
                      border-radius: 5px; 
                      margin-bottom: 20px;
                      text-align: center;
                    }
                    .status { 
                      background: #e7f3ff; 
                      padding: 15px; 
                      border-left: 4px solid #2196F3;
                      margin: 20px 0;
                    }
                    .loading {
                      display: inline-block;
                      width: 20px;
                      height: 20px;
                      border: 3px solid #f3f3f3;
                      border-top: 3px solid #3498db;
                      border-radius: 50%;
                      animation: spin 1s linear infinite;
                      margin-left: 10px;
                      vertical-align: middle;
                    }
                    @keyframes spin {
                      0% { transform: rotate(0deg); }
                      100% { transform: rotate(360deg); }
                    }
                    .code-fallback {
                      font-family: monospace;
                      background: #f5f5f5;
                      padding: 10px;
                      margin: 10px 0;
                      word-break: break-all;
                      display: none;
                    }
                  </style>
                </head>
                <body>
                  <div class="success-message">
                    <h1>✅ Authorization Successful!</h1>
                  </div>
                  
                  <div class="status" id="status">
                    <h3>Completing authentication automatically...</h3>
                    <p>Please wait while we complete the authentication process <span class="loading"></span></p>
                  </div>
                  
                  <div class="code-fallback" id="codeFallback">
                    <p>If automatic authentication fails, you can manually copy this code:</p>
                    <code>${code}</code>
                  </div>
                  
                  <script>
                    // Automatically submit the authorization code to complete the flow
                    async function completeAuth() {
                      try {
                        // Send the code to a special endpoint that will trigger the promise resolution
                        const response = await fetch('/complete-auth', {
                          method: 'POST',
                          headers: {
                            'Content-Type': 'application/json',
                          },
                          body: JSON.stringify({ 
                            code: '${code}',
                            state: '${state}'
                          })
                        });
                        
                        if (response.ok) {
                          document.getElementById('status').innerHTML = 
                            '<h3>✅ Authentication Complete!</h3>' +
                            '<p>You can now close this window and return to Claude Desktop.</p>';
                        } else {
                          throw new Error('Failed to complete authentication');
                        }
                      } catch (error) {
                        console.error('Auto-complete failed:', error);
                        document.getElementById('status').innerHTML = 
                          '<h3>⚠️ Automatic completion failed</h3>' +
                          '<p>Please copy the code below and paste it back to Claude Desktop:</p>';
                        document.getElementById('codeFallback').style.display = 'block';
                      }
                    }
                    
                    // Start the auto-completion process
                    setTimeout(completeAuth, 500);
                  </script>
                </body>
              </html>
            `);
            
            // Immediately trigger the authentication completion
            // by posting to our own complete-auth endpoint
            // Don't resolve here anymore - let the auto-complete endpoint handle it
            return;
          }
        }
        
        res.writeHead(404, { 'Content-Type': 'text/html' });
        res.end('<html><body><h1>Not Found</h1></body></html>');
      });
      
      this.server.listen(this.port, () => {
        this.isRunning = true;
        logger.info(`OAuth callback server listening on http://localhost:${this.port}`);
        resolve();
      });
      
      this.server.on('error', (err) => {
        this.isRunning = false;
        reject(err);
      });
    });
  }
  
  async waitForAuthorizationCode(sessionId: string = 'default'): Promise<string> {
    await this.ensureServerRunning();
    
    return new Promise((resolve, reject) => {
      // Store the promise resolvers for this session
      this.pendingPromises.set(sessionId, { resolve, reject });
      
      // Set a timeout to avoid hanging forever
      setTimeout(() => {
        if (this.pendingPromises.has(sessionId)) {
          this.pendingPromises.delete(sessionId);
          reject(new Error('OAuth timeout - no authorization received within 5 minutes'));
        }
      }, 5 * 60 * 1000); // 5 minutes timeout
    });
  }
  
  getCallbackUrl(): string {
    return `http://localhost:${this.port}`;
  }
  
  isServerRunning(): boolean {
    return this.isRunning;
  }

  setAuthHandler(handler: (code: string, state: string) => Promise<void>) {
    this.authHandler = handler;
  }
}

```

--------------------------------------------------------------------------------
/src/modules/drive/service.ts:
--------------------------------------------------------------------------------

```typescript
import { google } from 'googleapis';
import { BaseGoogleService } from '../../services/base/BaseGoogleService.js';
import { DriveOperationResult, FileDownloadOptions, FileListOptions, FileSearchOptions, FileUploadOptions, PermissionOptions } from './types.js';
import { Readable } from 'stream';
import { DRIVE_SCOPES } from './scopes.js';
import { workspaceManager } from '../../utils/workspace.js';
import fs from 'fs/promises';
import { GaxiosResponse } from 'gaxios';

export class DriveService extends BaseGoogleService<ReturnType<typeof google.drive>> {
  private initialized = false;

  constructor() {
    super({
      serviceName: 'Google Drive',
      version: 'v3'
    });
  }

  /**
   * Initialize the Drive service and all dependencies
   */
  public async initialize(): Promise<void> {
    try {
      await super.initialize();
      this.initialized = true;
    } catch (error) {
      throw this.handleError(error, 'Failed to initialize Drive service');
    }
  }

  /**
   * Ensure the Drive service is initialized
   */
  public async ensureInitialized(): Promise<void> {
    if (!this.initialized) {
      await this.initialize();
    }
  }

  /**
   * Check if the service is initialized
   */
  private checkInitialized(): void {
    if (!this.initialized) {
      throw this.handleError(
        new Error('Drive service not initialized'),
        'Please ensure the service is initialized before use'
      );
    }
  }

  async listFiles(email: string, options: FileListOptions = {}): Promise<DriveOperationResult> {
    try {
      await this.ensureInitialized();
      this.checkInitialized();
      await this.validateScopes(email, [DRIVE_SCOPES.FILE]);
      const client = await this.getAuthenticatedClient(
        email,
        (auth) => google.drive({ version: 'v3', auth })
      );

      const query = [];
      if (options.folderId) {
        query.push(`'${options.folderId}' in parents`);
      }
      if (options.query) {
        query.push(options.query);
      }

      const response = await client.files.list({
        q: query.join(' and ') || undefined,
        pageSize: options.pageSize,
        orderBy: options.orderBy?.join(','),
        fields: options.fields?.join(',') || 'files(id, name, mimeType, modifiedTime, size)',
      });

      return {
        success: true,
        data: response.data,
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error occurred',
      };
    }
  }

  async uploadFile(email: string, options: FileUploadOptions): Promise<DriveOperationResult> {
    try {
      await this.ensureInitialized();
      await this.validateScopes(email, [DRIVE_SCOPES.FILE]);
      const client = await this.getAuthenticatedClient(
        email,
        (auth) => google.drive({ version: 'v3', auth })
      );

      // Save content to workspace first
      const uploadPath = await workspaceManager.getUploadPath(email, options.name);
      await fs.writeFile(uploadPath, options.content);

      const fileContent = await fs.readFile(uploadPath);
      const media = {
        mimeType: options.mimeType || 'application/octet-stream',
        body: Readable.from([fileContent]),
      };

      const response = await client.files.create({
        requestBody: {
          name: options.name,
          mimeType: options.mimeType,
          parents: options.parents,
        },
        media,
        fields: 'id, name, mimeType, webViewLink',
      });

      return {
        success: true,
        data: response.data,
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error occurred',
      };
    }
  }

  async downloadFile(email: string, options: FileDownloadOptions): Promise<DriveOperationResult> {
    try {
      await this.ensureInitialized();
      await this.validateScopes(email, [DRIVE_SCOPES.FILE]);
      const client = await this.getAuthenticatedClient(
        email,
        (auth) => google.drive({ version: 'v3', auth })
      );

      // First get file metadata to check mime type and name
      const file = await client.files.get({
        fileId: options.fileId,
        fields: 'name,mimeType',
      });

      const fileName = file.data.name || options.fileId;

      // Handle Google Workspace files differently
      if (file.data.mimeType?.startsWith('application/vnd.google-apps')) {
        let exportMimeType = options.mimeType || 'text/plain';
        
        // Default export formats if not specified
        if (!options.mimeType) {
          switch (file.data.mimeType) {
            case 'application/vnd.google-apps.document':
              exportMimeType = 'text/markdown';
              break;
            case 'application/vnd.google-apps.spreadsheet':
              exportMimeType = 'text/csv';
              break;
            case 'application/vnd.google-apps.presentation':
              exportMimeType = 'text/plain';
              break;
            case 'application/vnd.google-apps.drawing':
              exportMimeType = 'image/png';
              break;
          }
        }

        const response = await client.files.export({
          fileId: options.fileId,
          mimeType: exportMimeType
        }, {
          responseType: 'arraybuffer'
        }) as unknown as GaxiosResponse<Uint8Array>;

        const downloadPath = await workspaceManager.getDownloadPath(email, fileName);
        await fs.writeFile(downloadPath, Buffer.from(response.data));

        return {
          success: true,
          data: response.data,
          mimeType: exportMimeType,
          filePath: downloadPath
        };
      }

      // For regular files
      const response = await client.files.get({
        fileId: options.fileId,
        alt: 'media'
      }, {
        responseType: 'arraybuffer'
      }) as unknown as GaxiosResponse<Uint8Array>;

      const downloadPath = await workspaceManager.getDownloadPath(email, fileName);
      await fs.writeFile(downloadPath, Buffer.from(response.data));

      return {
        success: true,
        data: response.data,
        filePath: downloadPath
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error occurred',
      };
    }
  }

  async createFolder(email: string, name: string, parentId?: string): Promise<DriveOperationResult> {
    try {
      await this.ensureInitialized();
      await this.validateScopes(email, [DRIVE_SCOPES.FILE]);
      const client = await this.getAuthenticatedClient(
        email,
        (auth) => google.drive({ version: 'v3', auth })
      );

      const response = await client.files.create({
        requestBody: {
          name,
          mimeType: 'application/vnd.google-apps.folder',
          parents: parentId ? [parentId] : undefined,
        },
        fields: 'id, name, mimeType, webViewLink',
      });

      return {
        success: true,
        data: response.data,
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error occurred',
      };
    }
  }

  async searchFiles(email: string, options: FileSearchOptions): Promise<DriveOperationResult> {
    try {
      await this.ensureInitialized();
      await this.validateScopes(email, [DRIVE_SCOPES.FILE]);
      const client = await this.getAuthenticatedClient(
        email,
        (auth) => google.drive({ version: 'v3', auth })
      );

      const query = [];
      
      if (options.fullText) {
        const escapedQuery = options.fullText.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
        query.push(`fullText contains '${escapedQuery}'`);
      }
      if (options.mimeType) {
        query.push(`mimeType = '${options.mimeType}'`);
      }
      if (options.folderId) {
        query.push(`'${options.folderId}' in parents`);
      }
      if (options.trashed !== undefined) {
        query.push(`trashed = ${options.trashed}`);
      }
      if (options.query) {
        query.push(options.query);
      }

      const response = await client.files.list({
        q: query.join(' and ') || undefined,
        pageSize: options.pageSize,
        orderBy: options.orderBy?.join(','),
        fields: options.fields?.join(',') || 'files(id, name, mimeType, modifiedTime, size)',
      });

      return {
        success: true,
        data: response.data,
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error occurred',
      };
    }
  }

  async updatePermissions(email: string, options: PermissionOptions): Promise<DriveOperationResult> {
    try {
      await this.ensureInitialized();
      await this.validateScopes(email, [DRIVE_SCOPES.FILE]);
      const client = await this.getAuthenticatedClient(
        email,
        (auth) => google.drive({ version: 'v3', auth })
      );

      const response = await client.permissions.create({
        fileId: options.fileId,
        requestBody: {
          role: options.role,
          type: options.type,
          emailAddress: options.emailAddress,
          domain: options.domain,
          allowFileDiscovery: options.allowFileDiscovery,
        },
      });

      return {
        success: true,
        data: response.data,
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error occurred',
      };
    }
  }

  async deleteFile(email: string, fileId: string): Promise<DriveOperationResult> {
    try {
      await this.ensureInitialized();
      await this.validateScopes(email, [DRIVE_SCOPES.FILE]);
      const client = await this.getAuthenticatedClient(
        email,
        (auth) => google.drive({ version: 'v3', auth })
      );

      await client.files.delete({
        fileId,
      });

      return {
        success: true,
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error occurred',
      };
    }
  }
}

```

--------------------------------------------------------------------------------
/src/modules/gmail/services/draft.ts:
--------------------------------------------------------------------------------

```typescript

import { google } from 'googleapis';
import { 
  GmailError, 
  OutgoingGmailAttachment,
  IncomingGmailAttachment 
} from '../types.js';
import { GmailAttachmentService } from './attachment.js';

export type DraftAction = 'create' | 'read' | 'update' | 'delete' | 'send';

export interface ManageDraftParams {
  email: string;
  action: DraftAction;
  draftId?: string;
  data?: DraftData;
}

export interface DraftData {
  to: string[];
  subject: string;
  body: string;
  cc?: string[];
  bcc?: string[];
  threadId?: string; // For reply drafts
  attachments?: OutgoingGmailAttachment[];
}

export class DraftService {
  private gmailClient?: ReturnType<typeof google.gmail>;
  constructor(private attachmentService: GmailAttachmentService) {}

  async initialize(): Promise<void> {
    // Initialization will be handled by Gmail service
  }

  updateClient(client: ReturnType<typeof google.gmail>) {
    this.gmailClient = client;
  }

  private ensureClient(): ReturnType<typeof google.gmail> {
    if (!this.gmailClient) {
      throw new GmailError(
        'Gmail client not initialized',
        'CLIENT_ERROR',
        'Please ensure the service is initialized'
      );
    }
    return this.gmailClient;
  }

  async createDraft(email: string, data: DraftData) {
    try {
      const client = this.ensureClient();

      // Validate and prepare attachments
      const processedAttachments = data.attachments?.map(attachment => {
        this.attachmentService.validateAttachment(attachment);
        return this.attachmentService.prepareAttachment(attachment);
      }) || [];

      // Construct email with attachments
      const boundary = `boundary_${Date.now()}`;
      const messageParts = [
        'MIME-Version: 1.0\n',
        `Content-Type: multipart/mixed; boundary="${boundary}"\n`,
        `To: ${data.to.join(', ')}\n`,
        data.cc?.length ? `Cc: ${data.cc.join(', ')}\n` : '',
        data.bcc?.length ? `Bcc: ${data.bcc.join(', ')}\n` : '',
        `Subject: ${data.subject}\n\n`,
        `--${boundary}\n`,
        'Content-Type: text/plain; charset="UTF-8"\n',
        'Content-Transfer-Encoding: 7bit\n\n',
        data.body,
        '\n'
      ];

      // Add attachments
      for (const attachment of processedAttachments) {
        messageParts.push(
          `--${boundary}\n`,
          `Content-Type: ${attachment.mimeType}\n`,
          'Content-Transfer-Encoding: base64\n',
          `Content-Disposition: attachment; filename="${attachment.filename}"\n\n`,
            attachment.content.toString(),
          '\n'
        );
      }

      messageParts.push(`--${boundary}--`);
      const fullMessage = messageParts.join('');

      // Create draft with threadId if it's a reply
      const { data: draft } = await client.users.drafts.create({
        userId: 'me',
        requestBody: {
          message: {
            raw: Buffer.from(fullMessage).toString('base64'),
            threadId: data.threadId // Include threadId for replies
          }
        }
      });

      return {
        id: draft.id || '',
        message: {
          id: draft.message?.id || '',
          threadId: draft.message?.threadId || '',
          labelIds: draft.message?.labelIds || []
        },
        updated: new Date().toISOString(),
        attachments: data.attachments
      };
    } catch (error) {
      throw new GmailError(
        'Failed to create draft',
        'CREATE_ERROR',
        error instanceof Error ? error.message : 'Unknown error'
      );
    }
  }

  async listDrafts(email: string) {
    try {
      const client = this.ensureClient();
      const { data } = await client.users.drafts.list({
        userId: 'me'
      });

      // Get full details for each draft
      const drafts = await Promise.all((data.drafts || [])
        .filter((draft): draft is { id: string } => typeof draft.id === 'string')
        .map(async draft => {
          try {
            return await this.getDraft(email, draft.id);
          } catch (error) {
            // Log error but continue with other drafts
            console.error(`Failed to get draft ${draft.id}:`, error);
            return null;
          }
        })
      );

      // Filter out any failed draft fetches
      const successfulDrafts = drafts.filter((draft): draft is NonNullable<typeof draft> => draft !== null);

      return {
        drafts: successfulDrafts,
        nextPageToken: data.nextPageToken || undefined,
        resultSizeEstimate: data.resultSizeEstimate || 0
      };
    } catch (error) {
      throw new GmailError(
        'Failed to list drafts',
        'LIST_ERROR',
        error instanceof Error ? error.message : 'Unknown error'
      );
    }
  }

  async getDraft(email: string, draftId: string) {
    try {
      const client = this.ensureClient();
      const { data } = await client.users.drafts.get({
        userId: 'me',
        id: draftId,
        format: 'full'
      });

      if (!data.id || !data.message?.id || !data.message?.threadId) {
        throw new GmailError(
          'Invalid response from Gmail API',
          'GET_ERROR',
          'Message ID or Thread ID is missing'
        );
      }

      return {
        id: data.id,
        message: {
          id: data.message.id,
          threadId: data.message.threadId,
          labelIds: data.message.labelIds || []
        },
        updated: new Date().toISOString() // Gmail API doesn't provide updated time, using current time
      };
    } catch (error) {
      throw new GmailError(
        'Failed to get draft',
        'GET_ERROR',
        error instanceof Error ? error.message : 'Unknown error'
      );
    }
  }

  async updateDraft(email: string, draftId: string, data: DraftData) {
    try {
      const client = this.ensureClient();

      // Validate and prepare attachments
      const processedAttachments = data.attachments?.map(attachment => {
        this.attachmentService.validateAttachment(attachment);
        return this.attachmentService.prepareAttachment(attachment);
      }) || [];

      // Construct updated email
      const boundary = `boundary_${Date.now()}`;
      const messageParts = [
        'MIME-Version: 1.0\n',
        `Content-Type: multipart/mixed; boundary="${boundary}"\n`,
        `To: ${data.to.join(', ')}\n`,
        data.cc?.length ? `Cc: ${data.cc.join(', ')}\n` : '',
        data.bcc?.length ? `Bcc: ${data.bcc.join(', ')}\n` : '',
        `Subject: ${data.subject}\n\n`,
        `--${boundary}\n`,
        'Content-Type: text/plain; charset="UTF-8"\n',
        'Content-Transfer-Encoding: 7bit\n\n',
        data.body,
        '\n'
      ];

      // Add attachments
      for (const attachment of processedAttachments) {
        messageParts.push(
          `--${boundary}\n`,
          `Content-Type: ${attachment.mimeType}\n`,
          'Content-Transfer-Encoding: base64\n',
          `Content-Disposition: attachment; filename="${attachment.filename}"\n\n`,
          attachment.content,
          '\n'
        );
      }

      messageParts.push(`--${boundary}--`);
      const fullMessage = messageParts.join('');

      // Update draft
      const { data: draft } = await client.users.drafts.update({
        userId: 'me',
        id: draftId,
        requestBody: {
          message: {
            raw: Buffer.from(fullMessage).toString('base64')
          }
        }
      });

      return {
        id: draft.id || '',
        message: {
          id: draft.message?.id || '',
          threadId: draft.message?.threadId || '',
          labelIds: draft.message?.labelIds || []
        },
        updated: new Date().toISOString(),
        attachments: data.attachments
      };
    } catch (error) {
      throw new GmailError(
        'Failed to update draft',
        'UPDATE_ERROR',
        error instanceof Error ? error.message : 'Unknown error'
      );
    }
  }

  async deleteDraft(email: string, draftId: string) {
    try {
      const client = this.ensureClient();
      await client.users.drafts.delete({
        userId: 'me',
        id: draftId
      });

      return;
    } catch (error) {
      throw new GmailError(
        'Failed to delete draft',
        'DELETE_ERROR',
        error instanceof Error ? error.message : 'Unknown error'
      );
    }
  }

  async manageDraft(params: ManageDraftParams) {
    const { email, action, draftId, data } = params;

    switch (action) {
      case 'create':
        if (!data) {
          throw new GmailError(
            'Draft data is required for create action',
            'INVALID_PARAMS'
          );
        }
        return this.createDraft(email, data);

      case 'read':
        if (!draftId) {
          return this.listDrafts(email);
        }
        return this.getDraft(email, draftId);

      case 'update':
        if (!draftId || !data) {
          throw new GmailError(
            'Draft ID and data are required for update action',
            'INVALID_PARAMS'
          );
        }
        return this.updateDraft(email, draftId, data);

      case 'delete':
        if (!draftId) {
          throw new GmailError(
            'Draft ID is required for delete action',
            'INVALID_PARAMS'
          );
        }
        return this.deleteDraft(email, draftId);

      case 'send':
        if (!draftId) {
          throw new GmailError(
            'Draft ID is required for send action',
            'INVALID_PARAMS'
          );
        }
        return this.sendDraft(email, draftId);

      default:
        throw new GmailError(
          'Invalid action',
          'INVALID_PARAMS',
          'Supported actions are: create, read, update, delete, send'
        );
    }
  }

  async sendDraft(email: string, draftId: string) {
    try {
      const client = this.ensureClient();
      const { data } = await client.users.drafts.send({
        userId: 'me',
        requestBody: {
          id: draftId
        }
      });

      if (!data.id || !data.threadId) {
        throw new GmailError(
          'Invalid response from Gmail API',
          'SEND_ERROR',
          'Message ID or Thread ID is missing'
        );
      }
      
      return {
        messageId: data.id,
        threadId: data.threadId,
        labelIds: data.labelIds || undefined
      };
    } catch (error) {
      throw new GmailError(
        'Failed to send draft',
        'SEND_ERROR',
        error instanceof Error ? error.message : 'Unknown error'
      );
    }
  }
}

```

--------------------------------------------------------------------------------
/src/__tests__/modules/calendar/service.test.ts:
--------------------------------------------------------------------------------

```typescript
import { CalendarService } from '../../../modules/calendar/service.js';
import { calendar_v3 } from 'googleapis';
import { getAccountManager } from '../../../modules/accounts/index.js';
import { AccountManager } from '../../../modules/accounts/manager.js';
import { CreateEventParams } from '../../../modules/calendar/types.js';

jest.mock('../../../modules/accounts/index.js');
jest.mock('../../../modules/accounts/manager.js');

describe('CalendarService', () => {
  let calendarService: CalendarService;
  let mockCalendarClient: jest.Mocked<calendar_v3.Calendar>;
  let mockAccountManager: jest.Mocked<AccountManager>;
  const mockEmail = '[email protected]';

  beforeEach(() => {
    // Simplified mock setup with proper typing
    mockCalendarClient = {
      events: {
        list: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
        get: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
        insert: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
        patch: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
      },
    } as unknown as jest.Mocked<calendar_v3.Calendar>;

    mockAccountManager = {
      validateToken: jest.fn().mockResolvedValue({ valid: true, token: {} }),
      getAuthClient: jest.fn().mockResolvedValue({}),
    } as unknown as jest.Mocked<AccountManager>;

    (getAccountManager as jest.Mock).mockReturnValue(mockAccountManager);
    calendarService = new CalendarService();
    (calendarService as any).getCalendarClient = jest.fn().mockResolvedValue(mockCalendarClient);
  });

  describe('getEvents', () => {
    it('should return events list', async () => {
      const mockEvents = [
        { id: 'event1', summary: 'Test Event 1' },
        { id: 'event2', summary: 'Test Event 2' }
      ];
      
      (mockCalendarClient.events.list as jest.Mock).mockImplementation(() => 
        Promise.resolve({ data: { items: mockEvents } })
      );

      const result = await calendarService.getEvents({ email: mockEmail });

      expect(result).toEqual(expect.arrayContaining([
        expect.objectContaining({ id: 'event1' }),
        expect.objectContaining({ id: 'event2' })
      ]));
    });

    it('should handle empty results', async () => {
      (mockCalendarClient.events.list as jest.Mock).mockImplementation(() => 
        Promise.resolve({ data: {} })
      );
      const result = await calendarService.getEvents({ email: mockEmail });
      expect(result).toEqual([]);
    });

    it('should handle invalid date format', async () => {
      await expect(calendarService.getEvents({
        email: mockEmail,
        timeMin: 'invalid-date'
      })).rejects.toThrow('Invalid date format');
    });
  });

  describe('createEvent', () => {
    const mockEvent = {
      email: mockEmail,
      summary: 'Meeting',
      start: { dateTime: '2024-01-15T10:00:00Z' },
      end: { dateTime: '2024-01-15T11:00:00Z' }
    };

    it('should create event', async () => {
      (mockCalendarClient.events.insert as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: { id: 'new-1', summary: 'Meeting', htmlLink: 'url' }
        })
      );

      const result = await calendarService.createEvent(mockEvent);

      expect(result).toEqual(expect.objectContaining({
        id: 'new-1',
        summary: 'Meeting'
      }));
    });

    it('should handle creation failure', async () => {
      (mockCalendarClient.events.insert as jest.Mock).mockImplementation(() =>
        Promise.reject(new Error('Failed'))
      );
      await expect(calendarService.createEvent(mockEvent)).rejects.toThrow();
    });
  });

  describe('manageEvent', () => {
    beforeEach(() => {
      (mockCalendarClient.events.get as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: {
            id: 'event1',
            summary: 'Test Event',
            attendees: [{ email: mockEmail }]
          }
        })
      );
    });

    it('should accept event', async () => {
      (mockCalendarClient.events.patch as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: { id: 'event1', status: 'accepted' }
        })
      );

      const result = await calendarService.manageEvent({
        email: mockEmail,
        eventId: 'event1',
        action: 'accept'
      });

      expect(result.success).toBe(true);
      expect(result.status).toBe('completed');
    });

    it('should handle invalid action', async () => {
      await expect(calendarService.manageEvent({
        email: mockEmail,
        eventId: 'event1',
        action: 'invalid_action' as any
      })).rejects.toThrow();
    });

    it('should validate new times for propose action', async () => {
      await expect(calendarService.manageEvent({
        email: mockEmail,
        eventId: 'event1',
        action: 'propose_new_time'
      })).rejects.toThrow('No proposed times provided');
    });
  });

  describe('getEvent', () => {
    it('should get single event', async () => {
      (mockCalendarClient.events.get as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: { id: 'event1', summary: 'Test' }
        })
      );

      const result = await calendarService.getEvent(mockEmail, 'event1');
      expect(result).toEqual(expect.objectContaining({ id: 'event1' }));
    });

    it('should handle not found', async () => {
      (mockCalendarClient.events.get as jest.Mock).mockImplementation(() =>
        Promise.reject(new Error('Not found'))
      );
      await expect(calendarService.getEvent(mockEmail, 'nonexistent'))
        .rejects.toThrow();
    });
  });

  describe('deleteEvent', () => {
    beforeEach(() => {
      // Add delete method to mock
      mockCalendarClient.events.delete = jest.fn().mockResolvedValue({});
    });

    it('should delete a single event with default parameters', async () => {
      await calendarService.deleteEvent(mockEmail, 'event1');
      
      expect(mockCalendarClient.events.delete).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event1',
        sendUpdates: 'all'
      });
    });

    it('should delete a single event with custom sendUpdates', async () => {
      await calendarService.deleteEvent(mockEmail, 'event1', 'none');
      
      expect(mockCalendarClient.events.delete).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event1',
        sendUpdates: 'none'
      });
    });

    it('should delete entire series of a recurring event', async () => {
      await calendarService.deleteEvent(mockEmail, 'recurring1', 'all', 'entire_series');
      
      expect(mockCalendarClient.events.delete).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'recurring1',
        sendUpdates: 'all'
      });
    });

    it('should handle "this_and_following" for a recurring event instance', async () => {
      // Mock a recurring event instance
      (mockCalendarClient.events.get as jest.Mock).mockResolvedValueOnce({
        data: {
          id: 'instance1',
          recurringEventId: 'master1',
          start: { dateTime: '2024-05-15T10:00:00Z' }
        }
      });

      // Mock the master event
      (mockCalendarClient.events.get as jest.Mock).mockResolvedValueOnce({
        data: {
          id: 'master1',
          recurrence: ['RRULE:FREQ=WEEKLY;COUNT=10'],
          start: { dateTime: '2024-05-01T10:00:00Z' }
        }
      });

      await calendarService.deleteEvent(mockEmail, 'instance1', 'all', 'this_and_following');
      
      // Should get the instance
      expect(mockCalendarClient.events.get).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'instance1'
      });

      // Should get the master event
      expect(mockCalendarClient.events.get).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'master1'
      });

      // Should update the master event's recurrence rule
      expect(mockCalendarClient.events.patch).toHaveBeenCalledWith(
        expect.objectContaining({
          calendarId: 'primary',
          eventId: 'master1',
          requestBody: {
            recurrence: expect.arrayContaining([
              expect.stringMatching(/RRULE:FREQ=WEEKLY;COUNT=10;UNTIL=\d+/)
            ])
          }
        })
      );

      // Should delete the instance
      expect(mockCalendarClient.events.delete).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'instance1',
        sendUpdates: 'all'
      });
    });

    it('should handle "this_and_following" for a master recurring event', async () => {
      // Mock a master recurring event
      (mockCalendarClient.events.get as jest.Mock).mockResolvedValueOnce({
        data: {
          id: 'master1',
          recurrence: ['RRULE:FREQ=WEEKLY;COUNT=10'],
          start: { dateTime: '2024-05-01T10:00:00Z' }
        }
      });

      await calendarService.deleteEvent(mockEmail, 'master1', 'all', 'this_and_following');
      
      // Should update the recurrence rule
      expect(mockCalendarClient.events.patch).toHaveBeenCalledWith(
        expect.objectContaining({
          calendarId: 'primary',
          eventId: 'master1',
          requestBody: {
            recurrence: expect.arrayContaining([
              expect.stringMatching(/RRULE:FREQ=WEEKLY;COUNT=10;UNTIL=\d+/)
            ])
          }
        })
      );
    });

    it('should throw error when using "this_and_following" on a non-recurring event', async () => {
      // Mock a non-recurring event
      (mockCalendarClient.events.get as jest.Mock).mockResolvedValueOnce({
        data: {
          id: 'single1',
          summary: 'Single Event',
          start: { dateTime: '2024-05-01T10:00:00Z' }
        }
      });

      await expect(
        calendarService.deleteEvent(mockEmail, 'single1', 'all', 'this_and_following')
      ).rejects.toThrow('Deletion scope can only be applied to recurring events');
    });

    it('should handle errors gracefully', async () => {
      (mockCalendarClient.events.get as jest.Mock).mockRejectedValueOnce(new Error('API error'));
      (mockCalendarClient.events.delete as jest.Mock).mockResolvedValueOnce({});

      // Should fall back to simple delete
      await calendarService.deleteEvent(mockEmail, 'event1', 'all', 'this_and_following');
      
      expect(mockCalendarClient.events.delete).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event1',
        sendUpdates: 'all'
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/modules/gmail/services/email.ts:
--------------------------------------------------------------------------------

```typescript
import { google, gmail_v1 } from 'googleapis';
import {
  EmailResponse,
  GetEmailsParams,
  GetEmailsResponse,
  SendEmailParams,
  SendEmailResponse,
  ThreadInfo,
  GmailError,
  IncomingGmailAttachment,
  OutgoingGmailAttachment
} from '../types.js';
import { SearchService } from './search.js';
import { GmailAttachmentService } from './attachment.js';
import { AttachmentResponseTransformer } from '../../attachments/response-transformer.js';
import { AttachmentIndexService } from '../../attachments/index-service.js';

type GmailMessage = gmail_v1.Schema$Message;

export class EmailService {
  private responseTransformer: AttachmentResponseTransformer;

  constructor(
    private searchService: SearchService,
    private attachmentService: GmailAttachmentService,
    private gmailClient?: ReturnType<typeof google.gmail>
  ) {
    this.responseTransformer = new AttachmentResponseTransformer(AttachmentIndexService.getInstance());
  }

  /**
   * Updates the Gmail client instance
   * @param client - New Gmail client instance
   */
  updateClient(client: ReturnType<typeof google.gmail>) {
    this.gmailClient = client;
  }

  private ensureClient(): ReturnType<typeof google.gmail> {
    if (!this.gmailClient) {
      throw new GmailError(
        'Gmail client not initialized',
        'CLIENT_ERROR',
        'Please ensure the service is initialized'
      );
    }
    return this.gmailClient;
  }

  /**
   * Extracts all headers into a key-value map
   */
  private extractHeaders(headers: { name: string; value: string }[]): { [key: string]: string } {
    return headers.reduce((acc, header) => {
      acc[header.name] = header.value;
      return acc;
    }, {} as { [key: string]: string });
  }

  /**
   * Groups emails by thread ID and extracts thread information
   */
  private groupEmailsByThread(emails: EmailResponse[]): { [threadId: string]: ThreadInfo } {
    return emails.reduce((threads, email) => {
      if (!threads[email.threadId]) {
        threads[email.threadId] = {
          messages: [],
          participants: [],
          subject: email.subject,
          lastUpdated: email.date
        };
      }

      const thread = threads[email.threadId];
      thread.messages.push(email.id);
      
      if (!thread.participants.includes(email.from)) {
        thread.participants.push(email.from);
      }
      if (email.to && !thread.participants.includes(email.to)) {
        thread.participants.push(email.to);
      }
      
      const emailDate = new Date(email.date);
      const threadDate = new Date(thread.lastUpdated);
      if (emailDate > threadDate) {
        thread.lastUpdated = email.date;
      }

      return threads;
    }, {} as { [threadId: string]: ThreadInfo });
  }

  /**
   * Get attachment metadata from message parts
   */
  private getAttachmentMetadata(message: GmailMessage): IncomingGmailAttachment[] {
    const attachments: IncomingGmailAttachment[] = [];
    
    if (!message.payload?.parts) {
      return attachments;
    }

    for (const part of message.payload.parts) {
      if (part.filename && part.body?.attachmentId) {
        attachments.push({
          id: part.body.attachmentId,
          name: part.filename,
          mimeType: part.mimeType || 'application/octet-stream',
          size: parseInt(String(part.body.size || '0'))
        });
      }
    }

    return attachments;
  }

  /**
   * Enhanced getEmails method with support for advanced search criteria and options
   */
  async getEmails({ email, search = {}, options = {}, messageIds }: GetEmailsParams): Promise<GetEmailsResponse> {
    try {
      const maxResults = options.maxResults || 10;
      
      let messages;
      let nextPageToken: string | undefined;
      
      if (messageIds && messageIds.length > 0) {
        messages = { messages: messageIds.map(id => ({ id })) };
      } else {
        // Build search query from criteria
        const query = this.searchService.buildSearchQuery(search);
        
        // List messages matching query
        const client = this.ensureClient();
        const { data } = await client.users.messages.list({
          userId: 'me',
          q: query,
          maxResults,
          pageToken: options.pageToken,
        });
        
        messages = data;
        nextPageToken = data.nextPageToken || undefined;
      }

      if (!messages.messages || messages.messages.length === 0) {
        return {
          emails: [],
          resultSummary: {
            total: 0,
            returned: 0,
            hasMore: false,
            searchCriteria: search
          }
        };
      }

      // Get full message details
      const emails = await Promise.all(
        messages.messages.map(async (message) => {
          const client = this.ensureClient();
          const { data: email } = await client.users.messages.get({
            userId: 'me',
            id: message.id!,
            format: options.format || 'full',
          });

          const headers = (email.payload?.headers || []).map(h => ({
            name: h.name || '',
            value: h.value || ''
          }));
          const subject = headers.find(h => h.name === 'Subject')?.value || '';
          const from = headers.find(h => h.name === 'From')?.value || '';
          const to = headers.find(h => h.name === 'To')?.value || '';
          const date = headers.find(h => h.name === 'Date')?.value || '';

          // Get email body
          let body = '';
          if (email.payload?.body?.data) {
            body = Buffer.from(email.payload.body.data, 'base64').toString();
          } else if (email.payload?.parts) {
            const textPart = email.payload.parts.find(part => part.mimeType === 'text/plain');
            if (textPart?.body?.data) {
              body = Buffer.from(textPart.body.data, 'base64').toString();
            }
          }

          // Get attachment metadata if present and store in index
          const hasAttachments = email.payload?.parts?.some(part => part.filename && part.filename.length > 0) || false;
          let attachments;
          if (hasAttachments) {
            attachments = this.getAttachmentMetadata(email);
            // Store each attachment's metadata in the index
            attachments.forEach(attachment => {
              this.attachmentService.addAttachment(email.id!, attachment);
            });
          }

          const response: EmailResponse = {
            id: email.id!,
            threadId: email.threadId!,
            labelIds: email.labelIds || undefined,
            snippet: email.snippet || undefined,
            subject,
            from,
            to,
            date,
            body,
            headers: options.includeHeaders ? this.extractHeaders(headers) : undefined,
            isUnread: email.labelIds?.includes('UNREAD') || false,
            hasAttachment: hasAttachments,
            attachments
          };

          return response;
        })
      );

      // Handle threaded view if requested
      const threads = options.threadedView ? this.groupEmailsByThread(emails) : undefined;

      // Sort emails if requested
      if (options.sortOrder) {
        emails.sort((a, b) => {
          const dateA = new Date(a.date).getTime();
          const dateB = new Date(b.date).getTime();
          return options.sortOrder === 'asc' ? dateA - dateB : dateB - dateA;
        });
      }

      // Transform response to simplify attachments
      const transformedResponse = this.responseTransformer.transformResponse({
        emails,
        nextPageToken,
        resultSummary: {
          total: messages.resultSizeEstimate || emails.length,
          returned: emails.length,
          hasMore: Boolean(nextPageToken),
          searchCriteria: search
        },
        threads
      });

      return transformedResponse;
    } catch (error) {
      if (error instanceof GmailError) {
        throw error;
      }
      throw new GmailError(
        'Failed to get emails',
        'FETCH_ERROR',
        `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
      );
    }
  }

  async sendEmail({ email, to, subject, body, cc = [], bcc = [], attachments = [] }: SendEmailParams): Promise<SendEmailResponse> {
    try {
      // Validate and prepare attachments for sending
      const processedAttachments = attachments?.map(attachment => {
        this.attachmentService.validateAttachment(attachment);
        const prepared = this.attachmentService.prepareAttachment(attachment);
        return {
          id: attachment.id,
          name: prepared.filename,
          mimeType: prepared.mimeType,
          size: attachment.size,
          content: prepared.content
        } as OutgoingGmailAttachment;
      }) || [];

      // Construct email with attachments
      const boundary = `boundary_${Date.now()}`;
      const messageParts = [
        'MIME-Version: 1.0\n',
        `Content-Type: multipart/mixed; boundary="${boundary}"\n`,
        `To: ${to.join(', ')}\n`,
        cc.length > 0 ? `Cc: ${cc.join(', ')}\n` : '',
        bcc.length > 0 ? `Bcc: ${bcc.join(', ')}\n` : '',
        `Subject: ${subject}\n\n`,
        `--${boundary}\n`,
        'Content-Type: text/plain; charset="UTF-8"\n',
        'Content-Transfer-Encoding: 7bit\n\n',
        body,
        '\n'
      ];

      // Add attachments directly from content
      for (const attachment of processedAttachments) {
        messageParts.push(
          `--${boundary}\n`,
          `Content-Type: ${attachment.mimeType}\n`,
          'Content-Transfer-Encoding: base64\n',
          `Content-Disposition: attachment; filename="${attachment.name}"\n\n`,
          attachment.content,
          '\n'
        );
      }

      messageParts.push(`--${boundary}--`);
      const fullMessage = messageParts.join('');

      // Encode the email in base64
      const encodedMessage = Buffer.from(fullMessage)
        .toString('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/, '');

      // Send the email
      const client = this.ensureClient();
      const { data } = await client.users.messages.send({
        userId: 'me',
        requestBody: {
          raw: encodedMessage,
        },
      });

      const response: SendEmailResponse = {
        messageId: data.id!,
        threadId: data.threadId!,
        labelIds: data.labelIds || undefined
      };

      if (processedAttachments.length > 0) {
        response.attachments = processedAttachments;
      }

      return response;
    } catch (error) {
      if (error instanceof GmailError) {
        throw error;
      }
      throw new GmailError(
        'Failed to send email',
        'SEND_ERROR',
        `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
      );
    }
  }
}

```

--------------------------------------------------------------------------------
/docs/API.md:
--------------------------------------------------------------------------------

```markdown
# Google Workspace MCP API Reference

IMPORTANT: Before using any workspace operations, you MUST call list_workspace_accounts first to:
1. Check for existing authenticated accounts
2. Determine which account to use if multiple exist
3. Verify required API scopes are authorized

## Account Management (Required First)

### list_workspace_accounts
List all configured Google workspace accounts and their authentication status.

This tool MUST be called first before any other workspace operations. It serves as the foundation for all account-based operations by:
1. Checking for existing authenticated accounts
2. Determining which account to use if multiple exist
3. Verifying required API scopes are authorized

Common Response Patterns:
- Valid account exists → Proceed with requested operation
- Multiple accounts exist → Ask user which to use
- Token expired → Proceed normally (auto-refresh occurs)
- No accounts exist → Start authentication flow

**Input Schema**: Empty object `{}`

**Output**: Array of account objects with authentication status

### authenticate_workspace_account
Add and authenticate a Google account for API access.

IMPORTANT: Only use this tool if list_workspace_accounts shows:
1. No existing accounts, OR
2. When using the account it seems to lack necessary auth scopes.

To prevent wasted time, DO NOT use this tool:
- Without checking list_workspace_accounts first
- When token is just expired (auto-refresh handles this)
- To re-authenticate an already valid account

**Input Schema**:
```typescript
{
  email: string;          // Required: Email address to authenticate
  category?: string;      // Optional: Account category (e.g., work, personal)
  description?: string;   // Optional: Account description
  auth_code?: string;     // Optional: OAuth code for completing authentication
}
```

### remove_workspace_account
Remove a Google account and delete associated tokens.

**Input Schema**:
```typescript
{
  email: string;  // Required: Email address to remove
}
```

## Gmail Operations

IMPORTANT: All Gmail operations require prior verification of account access using list_workspace_accounts.

### search_workspace_emails
Search emails with advanced filtering.

Response Format (v1.1):
- Attachments are simplified to just filename
- Full metadata is maintained internally
- Example response:
```json
{
  "id": "message123",
  "attachments": [{
    "name": "document.pdf"
  }]
}
```

Common Query Patterns:
- Meeting emails: "from:(*@zoom.us OR zoom.us OR [email protected]) subject:(meeting OR sync OR invite)"
- HR/Admin: "from:(*@workday.com OR *@adp.com) subject:(time off OR PTO OR benefits)"
- Team updates: "from:(*@company.com) -from:([email protected])"
- Newsletters: "subject:(newsletter OR digest) from:(*@company.com)"

Search Tips:
- Date format: YYYY-MM-DD (e.g., "2024-02-18")
- Labels: Case-sensitive, exact match (e.g., "INBOX", "SENT")
- Wildcards: Use * for partial matches (e.g., "*@domain.com")
- Operators: OR, -, (), has:attachment, larger:size, newer_than:date

**Input Schema**:
```typescript
{
  email: string;           // Required: Gmail account email
  search?: {              // Optional: Search criteria
    from?: string | string[];
    to?: string | string[];
    subject?: string;
    content?: string;     // Complex Gmail query
    after?: string;       // YYYY-MM-DD
    before?: string;      // YYYY-MM-DD
    hasAttachment?: boolean;
    labels?: string[];
    excludeLabels?: string[];
    includeSpam?: boolean;
    isUnread?: boolean;
  };
  maxResults?: number;    // Optional: Max results to return
}
```

### send_workspace_email
Send an email.

**Input Schema**:
```typescript
{
  email: string;           // Required: Sender email
  to: string[];           // Required: Recipients
  subject: string;        // Required: Email subject
  body: string;           // Required: Email content
  cc?: string[];         // Optional: CC recipients
  bcc?: string[];        // Optional: BCC recipients
}
```

### manage_workspace_draft
Manage email drafts.

**Input Schema**:
```typescript
{
  email: string;          // Required: Gmail account
  action: 'create' | 'read' | 'update' | 'delete' | 'send';
  draftId?: string;      // Required for read/update/delete/send
  data?: {               // Required for create/update
    to?: string[];
    subject?: string;
    body?: string;
    cc?: string[];
    bcc?: string[];
    replyToMessageId?: string;
    threadId?: string;
  }
}
```

## Calendar Operations

IMPORTANT: All Calendar operations require prior verification of account access using list_workspace_accounts.

### list_workspace_calendar_events
List calendar events.

Common Usage Patterns:
- Default view: Current week's events
- Specific range: Use timeMin/timeMax
- Search: Use query for text search

Example Flows:
1. User asks "check my calendar":
   - Verify account access
   - Show current week by default
   - Include upcoming events

2. User asks "find meetings about project":
   - Check account access
   - Search with relevant query
   - Focus on recent/upcoming events

**Input Schema**:
```typescript
{
  email: string;          // Required: Calendar owner email
  query?: string;        // Optional: Text search
  maxResults?: number;   // Optional: Max events to return
  timeMin?: string;      // Optional: Start time (ISO string)
  timeMax?: string;      // Optional: End time (ISO string)
}
```

### create_workspace_calendar_event
Create a calendar event.

**Input Schema**:
```typescript
{
  email: string;          // Required: Calendar owner
  summary: string;        // Required: Event title
  description?: string;   // Optional: Event description
  start: {               // Required: Start time
    dateTime: string;    // ISO-8601 format
    timeZone?: string;   // IANA timezone
  };
  end: {                 // Required: End time
    dateTime: string;    // ISO-8601 format
    timeZone?: string;   // IANA timezone
  };
  attendees?: {          // Optional: Event attendees
    email: string;
  }[];
  recurrence?: string[]; // Optional: RRULE strings
}
```

## Drive Operations

IMPORTANT: All Drive operations require prior verification of account access using list_workspace_accounts.

### list_drive_files
List files in Google Drive.

Common Usage Patterns:
- List all files: No options needed
- List folder contents: Provide folderId
- Custom queries: Use query parameter

Example Flow:
1. Check account access
2. Apply any filters
3. Return file list with metadata

**Input Schema**:
```typescript
{
  email: string;           // Required: Drive account email
  options?: {             // Optional: List options
    folderId?: string;    // Filter by parent folder
    query?: string;       // Custom query string
    pageSize?: number;    // Max files to return
    orderBy?: string[];   // Sort fields
    fields?: string[];    // Response fields to include
  }
}
```

### search_drive_files
Search files with advanced filtering.

**Input Schema**:
```typescript
{
  email: string;           // Required: Drive account email
  options: {              // Required: Search options
    fullText?: string;    // Full text search
    mimeType?: string;    // Filter by file type
    folderId?: string;    // Filter by parent folder
    trashed?: boolean;    // Include trashed files
    query?: string;       // Additional query string
    pageSize?: number;    // Max results
    orderBy?: string[];   // Sort order
    fields?: string[];    // Response fields
  }
}
```

### upload_drive_file
Upload a file to Drive.

**Input Schema**:
```typescript
{
  email: string;           // Required: Drive account email
  options: {              // Required: Upload options
    name: string;         // Required: File name
    content: string;      // Required: File content (string/base64)
    mimeType?: string;    // Optional: Content type
    parents?: string[];   // Optional: Parent folder IDs
  }
}
```

### download_drive_file
Download a file from Drive.

**Input Schema**:
```typescript
{
  email: string;           // Required: Drive account email
  fileId: string;         // Required: File to download
  mimeType?: string;      // Optional: Export format for Google files
}
```

### create_drive_folder
Create a new folder.

**Input Schema**:
```typescript
{
  email: string;           // Required: Drive account email
  name: string;           // Required: Folder name
  parentId?: string;      // Optional: Parent folder ID
}
```

### update_drive_permissions
Update file/folder sharing settings.

**Input Schema**:
```typescript
{
  email: string;           // Required: Drive account email
  options: {              // Required: Permission options
    fileId: string;       // Required: File/folder ID
    role: 'owner' | 'organizer' | 'fileOrganizer' | 
          'writer' | 'commenter' | 'reader';
    type: 'user' | 'group' | 'domain' | 'anyone';
    emailAddress?: string; // Required for user/group
    domain?: string;      // Required for domain
    allowFileDiscovery?: boolean;
  }
}
```

### delete_drive_file
Delete a file or folder.

**Input Schema**:
```typescript
{
  email: string;           // Required: Drive account email
  fileId: string;         // Required: File/folder to delete
}
```

## Label Management

### manage_workspace_label
Manage Gmail labels.

**Input Schema**:
```typescript
{
  email: string;           // Required: Gmail account
  action: 'create' | 'read' | 'update' | 'delete';
  labelId?: string;       // Required for read/update/delete
  data?: {               // Required for create/update
    name?: string;       // Label name
    messageListVisibility?: 'show' | 'hide';
    labelListVisibility?: 'labelShow' | 'labelHide' | 'labelShowIfUnread';
    color?: {
      textColor?: string;
      backgroundColor?: string;
    }
  }
}
```

### manage_workspace_label_assignment
Manage label assignments.

**Input Schema**:
```typescript
{
  email: string;           // Required: Gmail account
  action: 'add' | 'remove';
  messageId: string;      // Required: Message to modify
  labelIds: string[];     // Required: Labels to add/remove
}
```

### manage_workspace_label_filter
Manage Gmail filters.

**Input Schema**:
```typescript
{
  email: string;           // Required: Gmail account
  action: 'create' | 'read' | 'update' | 'delete';
  filterId?: string;      // Required for update/delete
  labelId?: string;       // Required for create/update
  data?: {
    criteria?: {
      from?: string[];
      to?: string[];
      subject?: string;
      hasWords?: string[];
      doesNotHaveWords?: string[];
      hasAttachment?: boolean;
      size?: {
        operator: 'larger' | 'smaller';
        size: number;
      }
    };
    actions?: {
      addLabel: boolean;
      markImportant?: boolean;
      markRead?: boolean;
      archive?: boolean;
    }
  }
}

```

--------------------------------------------------------------------------------
/ARCHITECTURE.md:
--------------------------------------------------------------------------------

```markdown
# Architecture

## Design Philosophy: Simplest Viable Design

This project follows the "simplest viable design" principle, which emerged from our experience with AI systems' tendency toward over-engineering, particularly in OAuth scope handling. This principle addresses a pattern we term "scope fondling" - where AI systems optimize for maximum anticipated flexibility rather than minimal necessary permissions.

Key aspects of this approach:
- Minimize complexity in permission structures
- Handle auth through simple HTTP response codes (401/403)
- Move OAuth mechanics entirely into platform infrastructure
- Present simple verb-noun interfaces to AI agents
- Focus on core operational requirements over edge cases

This principle helps prevent goal misgeneralization, where AI systems might otherwise create unnecessary complexity in authentication paths, connection management, and permission hierarchies.

## System Overview

The Google Workspace MCP Server implements a modular architecture with comprehensive support for Gmail, Calendar, and Drive services. The system is built around core modules that handle authentication, account management, and service-specific operations.

```mermaid
graph TB
    subgraph "Google Workspace MCP Tools"
        AM[Account Management]
        GM[Gmail Management]
        CM[Calendar Management]
        DM[Drive Management]
        
        subgraph "Account Tools"
            AM --> LA[list_workspace_accounts]
            AM --> AA[authenticate_workspace_account]
            AM --> RA[remove_workspace_account]
        end
        
        subgraph "Gmail Tools"
            GM --> SE[search_workspace_emails]
            GM --> SWE[send_workspace_email]
            GM --> GS[get_workspace_gmail_settings]
            GM --> MD[manage_workspace_draft]
            
            subgraph "Label Management"
                LM[Label Tools]
                LM --> ML[manage_workspace_label]
                LM --> MLA[manage_workspace_label_assignment]
                LM --> MLF[manage_workspace_label_filter]
            end
        end
        
        subgraph "Calendar Tools"
            CM --> LCE[list_workspace_calendar_events]
            CM --> GCE[get_workspace_calendar_event]
            CM --> MCE[manage_workspace_calendar_event]
            CM --> CCE[create_workspace_calendar_event]
            CM --> DCE[delete_workspace_calendar_event]
        end
        
        subgraph "Drive Tools"
            DM --> LDF[list_drive_files]
            DM --> SDF[search_drive_files]
            DM --> UDF[upload_drive_file]
            DM --> DDF[download_drive_file]
            DM --> CDF[create_drive_folder]
            DM --> UDP[update_drive_permissions]
            DM --> DEL[delete_drive_file]
        end
    end

    %% Service Dependencies
    LA -.->|Required First| SE
    LA -.->|Required First| CM
    LA -.->|Required First| DM
    AA -.->|Auth Flow| LA
```

Key characteristics:
- Authentication-first architecture with list_workspace_accounts as the foundation
- Comprehensive service modules for Gmail, Calendar, and Drive
- Integrated label management within Gmail
- Rich tool sets for each service domain

## Core Components

### 1. Scope Registry (src/modules/tools/scope-registry.ts)
- Simple scope collection system for OAuth
- Gathers required scopes at startup
- Used for initial auth setup and validation
- Handles runtime scope verification

### 2. MCP Server (src/index.ts)
- Registers and manages available tools
- Handles request routing and validation
- Provides consistent error handling
- Manages server lifecycle

### 3. Account Module (src/modules/accounts/*)
- OAuth Client:
  - Implements Google OAuth 2.0 flow
  - Handles token exchange and refresh
  - Provides authentication URLs
  - Manages client credentials
- Token Manager:
  - Handles token lifecycle
  - Validates and refreshes tokens
  - Manages token storage
- Account Manager:
  - Manages account configurations
  - Handles account persistence
  - Validates account status

### 4. Service Modules

#### Attachment System
- Singleton-based attachment management:
  - AttachmentIndexService: Central metadata cache with size limits
  - AttachmentResponseTransformer: Handles response simplification
  - AttachmentCleanupService: Manages cache expiry
- Cache Management:
  - Map-based storage using messageId + filename as key
  - Built-in size limit (256 entries)
  - Automatic expiry handling (1 hour timeout)
  - LRU-style cleanup for capacity management
- Abstraction Layers:
  - Service layer for Gmail/Calendar operations
  - Transformer layer for response formatting
  - Index layer for metadata storage
  - Cleanup layer for maintenance

#### Gmail Module (src/modules/gmail/*)
- Comprehensive email operations:
  - Email search and sending
  - Draft management
  - Label and filter control
  - Settings configuration
  - Simplified attachment handling
- Manages Gmail API integration
- Handles Gmail authentication scopes
- Integrates with attachment system

#### Calendar Module (src/modules/calendar/*)
- Complete calendar operations:
  - Event listing and search
  - Event creation and management
  - Event response handling
  - Recurring event support
- Manages Calendar API integration
- Handles calendar permissions

#### Drive Module (src/modules/drive/*)
- Full file management capabilities:
  - File listing and search
  - Upload and download
  - Folder management
  - Permission control
  - File operations (create, update, delete)
- Manages Drive API integration
- Handles file system operations

## Data Flows

### Operation Flow
```mermaid
sequenceDiagram
    participant TR as Tool Request
    participant S as Service
    participant API as Google API

    TR->>S: Request
    S->>API: API Call
    alt Success
        API-->>TR: Return Response
    else Auth Error (401/403)
        S->>S: Refresh Token
        S->>API: Retry API Call
        API-->>TR: Return Response
    end
```

### Auth Flow
```mermaid
sequenceDiagram
    participant TR as Tool Request
    participant S as Service
    participant AM as Account Manager
    participant API as Google API

    TR->>S: Request
    S->>API: API Call
    alt Success
        API-->>TR: Return Response
    else Auth Error
        S->>AM: Refresh Token
        alt Refresh Success
            S->>API: Retry API Call
            API-->>TR: Return Response
        else Refresh Failed
            AM-->>TR: Request Re-auth
        end
    end
```

## Implementation Details

### Testing Strategy

The project follows a simplified unit testing approach that emphasizes:

```mermaid
graph TD
    A[Unit Tests] --> B[Simplified Mocks]
    A --> C[Focused Tests]
    A --> D[Clean State]
    
    B --> B1[Static Responses]
    B --> B2[Simple File System]
    B --> B3[Basic OAuth]
    
    C --> C1[Grouped by Function]
    C --> C2[Single Responsibility]
    C --> C3[Clear Assertions]
    
    D --> D1[Reset Modules]
    D --> D2[Fresh Instances]
    D --> D3[Tracked Mocks]
```

#### Key Testing Principles

1. **Logging Strategy**
   - All logs are directed to stderr to maintain MCP protocol integrity
   - Prevents log messages from corrupting stdout JSON communication
   - Enables clean separation of logs and tool responses
   - Logger is mocked in tests to prevent console.error noise
   - Consistent logging approach across all modules

2. **Simplified Mocking**
   - Use static mock responses instead of complex simulations
   - Mock external dependencies with minimal implementations
   - Focus on behavior verification over implementation details
   - Avoid end-to-end complexity in unit tests

2. **Test Organization**
   - Group tests by functional area (e.g., account operations, file operations)
   - Each test verifies a single piece of functionality
   - Clear test descriptions that document behavior
   - Independent test cases that don't rely on shared state

3. **Mock Management**
   - Reset modules and mocks between tests
   - Track mock function calls explicitly
   - Re-require modules after mock changes
   - Verify both function calls and results

4. **File System Testing**
   - Use simple JSON structures
   - Focus on data correctness over formatting
   - Test error scenarios explicitly
   - Verify operations without implementation details

5. **Token Handling**
   - Mock token validation with static responses
   - Test success and failure scenarios separately
   - Focus on account manager's token handling logic
   - Avoid OAuth complexity in unit tests

This approach ensures tests are:
- Reliable and predictable
- Easy to maintain
- Quick to execute
- Clear in intent
- Focused on behavior

### Security
- OAuth 2.0 implementation with offline access
- Secure token storage and management
- Scope-based access control
- Environment-based configuration
- Secure credential handling

### Error Handling
- Simplified auth error handling through 401/403 responses
- Automatic token refresh on auth failures
- Service-specific error types
- Clear authentication error guidance

### Configuration
- OAuth credentials via environment variables
- Secure token storage in user's home directory
- Account configuration management
- Token persistence handling

## Project Structure

### Docker Container Structure
```
/app/
├── src/              # Application source code
│   ├── index.ts     # MCP server implementation
│   ├── modules/     # Core functionality modules
│   └── scripts/     # Utility scripts
├── config/          # Mount point for persistent data
│   ├── accounts.json     # Account configurations
│   └── credentials/     # Token storage
└── Dockerfile       # Container definition
```

### Local Development Structure
```
project/
├── src/             # Source code (mounted in container)
├── Dockerfile       # Container definition
└── docker-entrypoint.sh  # Container startup script
```

### Host Machine Structure
```
~/.mcp/google-workspace-mcp/  # Persistent data directory
├── accounts.json        # Account configurations
└── credentials/        # Token storage
```

## Configuration

### Container Environment Variables
```
GOOGLE_CLIENT_ID      - OAuth client ID
GOOGLE_CLIENT_SECRET  - OAuth client secret
GOOGLE_REDIRECT_URI   - OAuth redirect URI (optional)
```

### Volume Mounts
```
~/.mcp/google-workspace-mcp:/app/config  # Persistent data storage
```

### Data Directory Structure
The server uses a Docker volume mounted at `/app/config` to store:

1. Account Configuration (accounts.json):
```json
{
  "accounts": [{
    "email": "[email protected]",
    "category": "work",
    "description": "Work Account"
  }]
}
```

2. Credentials Directory:
- Contains OAuth tokens for each account
- Tokens are stored securely with appropriate permissions
- Each token file is named using the account's email address

## Version History

### Version 1.1
- Simplified attachment data in responses (filename only)
- Maintained full metadata in index service
- Improved attachment system architecture
- Enhanced documentation and examples
- Verified download functionality with simplified format

## Future Extensions

### Planned Services
- Admin SDK support for workspace management
- Additional Google Workspace integrations

### Planned Features
- Rate limiting
- Response caching
- Request logging
- Performance monitoring
- Multi-account optimization

```

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

```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import logger from '../utils/logger.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  Tool
} from "@modelcontextprotocol/sdk/types.js";

// Get docker hash from environment
const DOCKER_HASH = process.env.DOCKER_HASH || 'unknown';

// Import tool definitions and registry
import { allTools } from './definitions.js';
import { ToolRegistry } from '../modules/tools/registry.js';

// Import handlers
import {
  handleListWorkspaceAccounts,
  handleAuthenticateWorkspaceAccount,
  handleCompleteWorkspaceAuth,
  handleRemoveWorkspaceAccount
} from './account-handlers.js';

import {
  handleSearchWorkspaceEmails,
  handleSendWorkspaceEmail,
  handleGetWorkspaceGmailSettings,
  handleManageWorkspaceDraft,
  handleManageWorkspaceLabel,
  handleManageWorkspaceLabelAssignment,
  handleManageWorkspaceLabelFilter,
  handleManageWorkspaceAttachment
} from './gmail-handlers.js';

import {
  handleListWorkspaceCalendarEvents,
  handleGetWorkspaceCalendarEvent,
  handleManageWorkspaceCalendarEvent,
  handleCreateWorkspaceCalendarEvent,
  handleDeleteWorkspaceCalendarEvent
} from './calendar-handlers.js';

import {
  handleListDriveFiles,
  handleSearchDriveFiles,
  handleUploadDriveFile,
  handleDownloadDriveFile,
  handleCreateDriveFolder,
  handleUpdateDrivePermissions,
  handleDeleteDriveFile
} from './drive-handlers.js';

// Import contact handlers
import { handleGetContacts } from './contacts-handlers.js';

// Import error types
import { AccountError } from '../modules/accounts/types.js';
import { GmailError } from '../modules/gmail/types.js';
import { CalendarError } from '../modules/calendar/types.js';
import { ContactsError } from '../modules/contacts/types.js';

// Import service initializer
import { initializeAllServices } from '../utils/service-initializer.js';

// Import types and type guards
import {
  CalendarEventParams,
  SendEmailArgs,
  AuthenticateAccountArgs,
  ManageDraftParams,
  ManageAttachmentParams
} from './types.js';
import {
  ManageLabelParams,
  ManageLabelAssignmentParams,
  ManageLabelFilterParams
} from '../modules/gmail/services/label.js';

import {
  assertBaseToolArguments,
  assertCalendarEventParams,
  assertEmailEventIdArgs,
  assertSendEmailArgs,
  assertManageDraftParams,
  assertManageLabelParams,
  assertManageLabelAssignmentParams,
  assertManageLabelFilterParams,
  assertDriveFileListArgs,
  assertDriveSearchArgs,
  assertDriveUploadArgs,
  assertDriveDownloadArgs,
  assertDriveFolderArgs,
  assertDrivePermissionArgs,
  assertDriveDeleteArgs,
  assertManageAttachmentParams,
  assertGetContactsParams
} from './type-guards.js';

export class GSuiteServer {
  private server: Server;
  private toolRegistry: ToolRegistry;

  constructor() {
    this.toolRegistry = new ToolRegistry(allTools);
    this.server = new Server(
      {
        name: "Google Workspace MCP Server",
        version: "0.1.0"
      },
      {
        capabilities: {
          tools: {
            list: true,
            call: true
          }
        }
      }
    );

    this.setupRequestHandlers();
  }

  private setupRequestHandlers(): void {
    // Tools are registered through the ToolRegistry which serves as a single source of truth
    // for both tool discovery (ListToolsRequestSchema) and execution (CallToolRequestSchema).
    // Tools only need to be defined once in allTools and the registry handles making them
    // available to both handlers.
    
    // List available tools
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      // Get tools with categories organized
      const categories = this.toolRegistry.getCategories();
      const toolsByCategory: { [key: string]: Tool[] } = {};
      
      for (const category of categories) {
        // Convert ToolMetadata to Tool (strip out category and aliases for SDK compatibility)
        toolsByCategory[category.name] = category.tools.map(tool => ({
          name: tool.name,
          description: tool.description,
          inputSchema: tool.inputSchema
        }));
      }

      return {
        tools: allTools.map(tool => ({
          name: tool.name,
          description: tool.description,
          inputSchema: tool.inputSchema
        })),
        _meta: {
          categories: toolsByCategory,
          aliases: Object.fromEntries(
            allTools.flatMap(tool => 
              (tool.aliases || []).map(alias => [alias, tool.name])
            )
          )
        }
      };
    });

    // Handle tool calls
    this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
      try {
        const args = request.params.arguments || {};
        const toolName = request.params.name;
        
        // Look up the tool using the registry
        const tool = this.toolRegistry.getTool(toolName);
        if (!tool) {
          // Generate helpful error message with suggestions
          const errorMessage = this.toolRegistry.formatErrorWithSuggestions(toolName);
          throw new Error(errorMessage);
        }
        
        let result;
        // Use the canonical tool name for the switch
        switch (tool.name) {
          // Account Management
          case 'list_workspace_accounts':
            result = await handleListWorkspaceAccounts();
            break;
          case 'authenticate_workspace_account':
            result = await handleAuthenticateWorkspaceAccount(args as AuthenticateAccountArgs);
            break;
          case 'complete_workspace_auth':
            assertBaseToolArguments(args);
            result = await handleCompleteWorkspaceAuth(args);
            break;
          case 'remove_workspace_account':
            assertBaseToolArguments(args);
            result = await handleRemoveWorkspaceAccount(args);
            break;

          // Gmail Operations
          case 'search_workspace_emails':
            assertBaseToolArguments(args);
            result = await handleSearchWorkspaceEmails(args);
            break;
          case 'send_workspace_email':
            assertSendEmailArgs(args);
            result = await handleSendWorkspaceEmail(args as SendEmailArgs);
            break;
          case 'get_workspace_gmail_settings':
            assertBaseToolArguments(args);
            result = await handleGetWorkspaceGmailSettings(args);
            break;
          case 'manage_workspace_draft':
            assertManageDraftParams(args);
            result = await handleManageWorkspaceDraft(args as ManageDraftParams);
            break;

          case 'manage_workspace_attachment':
            assertManageAttachmentParams(args);
            result = await handleManageWorkspaceAttachment(args as ManageAttachmentParams);
            break;

          // Calendar Operations
          case 'list_workspace_calendar_events':
            assertCalendarEventParams(args);
            result = await handleListWorkspaceCalendarEvents(args as CalendarEventParams);
            break;
          case 'get_workspace_calendar_event':
            assertEmailEventIdArgs(args);
            result = await handleGetWorkspaceCalendarEvent(args);
            break;
          case 'manage_workspace_calendar_event':
            assertBaseToolArguments(args);
            result = await handleManageWorkspaceCalendarEvent(args);
            break;
          case 'create_workspace_calendar_event':
            assertBaseToolArguments(args);
            result = await handleCreateWorkspaceCalendarEvent(args);
            break;
          case 'delete_workspace_calendar_event':
            assertEmailEventIdArgs(args);
            result = await handleDeleteWorkspaceCalendarEvent(args);
            break;

          // Label Management
          case 'manage_workspace_label':
            assertManageLabelParams(args);
            result = await handleManageWorkspaceLabel(args as unknown as ManageLabelParams);
            break;
          case 'manage_workspace_label_assignment':
            assertManageLabelAssignmentParams(args);
            result = await handleManageWorkspaceLabelAssignment(args as unknown as ManageLabelAssignmentParams);
            break;
          case 'manage_workspace_label_filter':
            assertManageLabelFilterParams(args);
            result = await handleManageWorkspaceLabelFilter(args as unknown as ManageLabelFilterParams);
            break;

          // Drive Operations
          case 'list_drive_files':
            assertDriveFileListArgs(args);
            result = await handleListDriveFiles(args);
            break;
          case 'search_drive_files':
            assertDriveSearchArgs(args);
            result = await handleSearchDriveFiles(args);
            break;
          case 'upload_drive_file':
            assertDriveUploadArgs(args);
            result = await handleUploadDriveFile(args);
            break;
          case 'download_drive_file':
            assertDriveDownloadArgs(args);
            result = await handleDownloadDriveFile(args);
            break;
          case 'create_drive_folder':
            assertDriveFolderArgs(args);
            result = await handleCreateDriveFolder(args);
            break;
          case 'update_drive_permissions':
            assertDrivePermissionArgs(args);
            result = await handleUpdateDrivePermissions(args);
            break;
          case 'delete_drive_file':
            assertDriveDeleteArgs(args);
            result = await handleDeleteDriveFile(args);
            break;

          // Contact Operations
          case 'get_workspace_contacts':
            assertGetContactsParams(args);
            result = await handleGetContacts(args);
            break;

          default:
            throw new Error(`Unknown tool: ${request.params.name}`);
        }

        // Wrap result in McpToolResponse format
        // Handle undefined results (like from void functions)
        const responseText = result === undefined ? 
          JSON.stringify({ status: 'success', message: 'Operation completed successfully' }, null, 2) : 
          JSON.stringify(result, null, 2);
        
        return {
          content: [{
            type: 'text',
            text: responseText
          }],
          _meta: {}
        };
      } catch (error) {
        const response = this.formatErrorResponse(error);
        return {
          content: [{ type: 'text', text: JSON.stringify(response, null, 2) }],
          isError: true,
          _meta: {}
        };
      }
    });
  }

  private formatErrorResponse(error: unknown) {
    if (error instanceof AccountError || error instanceof GmailError || error instanceof CalendarError || error instanceof ContactsError) {
      const details = error instanceof GmailError ? error.details :
                     error instanceof AccountError ? error.resolution :
                     error instanceof CalendarError ? error.message :
                     error instanceof ContactsError ? error.details :
                     'Please try again or contact support if the issue persists';
      
      return {
        status: 'error',
        error: error.message,
        resolution: details
      };
    }

    return {
      status: 'error',
      error: error instanceof Error ? error.message : 'Unknown error occurred',
      resolution: 'Please try again or contact support if the issue persists'
    };
  }

  async run(): Promise<void> {
    try {
      // Initialize server
      logger.info(`google-workspace-mcp v0.9.0 (docker: ${DOCKER_HASH})`);
      
      // Initialize all services
      await initializeAllServices();
      
      // Set up error handler
      this.server.onerror = (error) => console.error('MCP Error:', error);
      
      // Connect transport
      const transport = new StdioServerTransport();
      await this.server.connect(transport);
      logger.info('Google Workspace MCP server running on stdio');
    } catch (error) {
      logger.error('Fatal server error:', error);
      throw error;
    }
  }
}

```

--------------------------------------------------------------------------------
/src/services/gmail/index.ts:
--------------------------------------------------------------------------------

```typescript
import { google } from 'googleapis';
import { BaseGoogleService } from '../base/BaseGoogleService.js';
import {
  GetEmailsParams,
  SendEmailParams,
  EmailResponse,
  SendEmailResponse,
  GetGmailSettingsParams,
  GetGmailSettingsResponse,
  SearchCriteria,
  GetEmailsResponse,
  ThreadInfo
} from '../../modules/gmail/types.js';

/**
 * Gmail service implementation extending BaseGoogleService.
 * Handles Gmail-specific operations while leveraging common Google API functionality.
 */
export class GmailService extends BaseGoogleService<ReturnType<typeof google.gmail>> {
  constructor() {
    super({
      serviceName: 'gmail',
      version: 'v1'
    });
  }

  /**
   * Gets an authenticated Gmail client
   */
  private async getGmailClient(email: string) {
    return this.getAuthenticatedClient(
      email,
      (auth) => google.gmail({ version: 'v1', auth })
    );
  }

  /**
   * Extracts all headers into a key-value map
   */
  private extractHeaders(headers: { name: string; value: string }[]): { [key: string]: string } {
    return headers.reduce((acc, header) => {
      acc[header.name] = header.value;
      return acc;
    }, {} as { [key: string]: string });
  }

  /**
   * Groups emails by thread ID and extracts thread information
   */
  private groupEmailsByThread(emails: EmailResponse[]): { [threadId: string]: ThreadInfo } {
    return emails.reduce((threads, email) => {
      if (!threads[email.threadId]) {
        threads[email.threadId] = {
          messages: [],
          participants: [],
          subject: email.subject,
          lastUpdated: email.date
        };
      }

      const thread = threads[email.threadId];
      thread.messages.push(email.id);
      
      if (!thread.participants.includes(email.from)) {
        thread.participants.push(email.from);
      }
      if (email.to && !thread.participants.includes(email.to)) {
        thread.participants.push(email.to);
      }
      
      const emailDate = new Date(email.date);
      const threadDate = new Date(thread.lastUpdated);
      if (emailDate > threadDate) {
        thread.lastUpdated = email.date;
      }

      return threads;
    }, {} as { [threadId: string]: ThreadInfo });
  }

  /**
   * Builds a Gmail search query string from SearchCriteria
   */
  private buildSearchQuery(criteria: SearchCriteria = {}): string {
    const queryParts: string[] = [];

    if (criteria.from) {
      const fromAddresses = Array.isArray(criteria.from) ? criteria.from : [criteria.from];
      if (fromAddresses.length === 1) {
        queryParts.push(`from:${fromAddresses[0]}`);
      } else {
        queryParts.push(`{${fromAddresses.map(f => `from:${f}`).join(' OR ')}}`);
      }
    }

    if (criteria.to) {
      const toAddresses = Array.isArray(criteria.to) ? criteria.to : [criteria.to];
      if (toAddresses.length === 1) {
        queryParts.push(`to:${toAddresses[0]}`);
      } else {
        queryParts.push(`{${toAddresses.map(t => `to:${t}`).join(' OR ')}}`);
      }
    }

    if (criteria.subject) {
      const escapedSubject = criteria.subject.replace(/["\\]/g, '\\$&');
      queryParts.push(`subject:"${escapedSubject}"`);
    }

    if (criteria.content) {
      const escapedContent = criteria.content.replace(/["\\]/g, '\\$&');
      queryParts.push(`"${escapedContent}"`);
    }

    if (criteria.after) {
      const afterDate = new Date(criteria.after);
      const afterStr = `${afterDate.getFullYear()}/${(afterDate.getMonth() + 1).toString().padStart(2, '0')}/${afterDate.getDate().toString().padStart(2, '0')}`;
      queryParts.push(`after:${afterStr}`);
    }
    if (criteria.before) {
      const beforeDate = new Date(criteria.before);
      const beforeStr = `${beforeDate.getFullYear()}/${(beforeDate.getMonth() + 1).toString().padStart(2, '0')}/${beforeDate.getDate().toString().padStart(2, '0')}`;
      queryParts.push(`before:${beforeStr}`);
    }

    if (criteria.hasAttachment) {
      queryParts.push('has:attachment');
    }

    if (criteria.labels && criteria.labels.length > 0) {
      criteria.labels.forEach(label => {
        queryParts.push(`label:${label}`);
      });
    }

    if (criteria.excludeLabels && criteria.excludeLabels.length > 0) {
      criteria.excludeLabels.forEach(label => {
        queryParts.push(`-label:${label}`);
      });
    }

    if (criteria.includeSpam) {
      queryParts.push('in:anywhere');
    }

    if (criteria.isUnread !== undefined) {
      queryParts.push(criteria.isUnread ? 'is:unread' : 'is:read');
    }

    return queryParts.join(' ');
  }

  /**
   * Gets emails with proper scope handling for search and content access.
   */
  async getEmails({ email, search = {}, options = {}, messageIds }: GetEmailsParams): Promise<GetEmailsResponse> {
    try {
      const gmail = await this.getGmailClient(email);
      const maxResults = options.maxResults || 10;
      
      let messages;
      let nextPageToken: string | undefined;
      
      if (messageIds && messageIds.length > 0) {
        messages = { messages: messageIds.map(id => ({ id })) };
      } else {
        const query = this.buildSearchQuery(search);
        
        const { data } = await gmail.users.messages.list({
          userId: 'me',
          q: query,
          maxResults,
          pageToken: options.pageToken,
        });
        
        messages = data;
        nextPageToken = data.nextPageToken || undefined;
      }

      if (!messages.messages || messages.messages.length === 0) {
        return {
          emails: [],
          resultSummary: {
            total: 0,
            returned: 0,
            hasMore: false,
            searchCriteria: search
          }
        };
      }

      const emails = await Promise.all(
        messages.messages.map(async (message) => {
          const { data: email } = await gmail.users.messages.get({
            userId: 'me',
            id: message.id!,
            format: options.format || 'full',
          });

          const headers = (email.payload?.headers || []).map(h => ({
            name: h.name || '',
            value: h.value || ''
          }));
          const subject = headers.find(h => h.name === 'Subject')?.value || '';
          const from = headers.find(h => h.name === 'From')?.value || '';
          const to = headers.find(h => h.name === 'To')?.value || '';
          const date = headers.find(h => h.name === 'Date')?.value || '';

          let body = '';
          if (email.payload?.body?.data) {
            body = Buffer.from(email.payload.body.data, 'base64').toString();
          } else if (email.payload?.parts) {
            const textPart = email.payload.parts.find(part => part.mimeType === 'text/plain');
            if (textPart?.body?.data) {
              body = Buffer.from(textPart.body.data, 'base64').toString();
            }
          }

          const response: EmailResponse = {
            id: email.id!,
            threadId: email.threadId!,
            labelIds: email.labelIds || undefined,
            snippet: email.snippet || undefined,
            subject,
            from,
            to,
            date,
            body,
            headers: options.includeHeaders ? this.extractHeaders(headers) : undefined,
            isUnread: email.labelIds?.includes('UNREAD') || false,
            hasAttachment: email.payload?.parts?.some(part => part.filename && part.filename.length > 0) || false
          };

          return response;
        })
      );

      const threads = options.threadedView ? this.groupEmailsByThread(emails) : undefined;

      if (options.sortOrder) {
        emails.sort((a, b) => {
          const dateA = new Date(a.date).getTime();
          const dateB = new Date(b.date).getTime();
          return options.sortOrder === 'asc' ? dateA - dateB : dateB - dateA;
        });
      }

      return {
        emails,
        nextPageToken,
        resultSummary: {
          total: messages.resultSizeEstimate || emails.length,
          returned: emails.length,
          hasMore: Boolean(nextPageToken),
          searchCriteria: search
        },
        threads
      };
    } catch (error) {
      throw this.handleError(error, 'Failed to get emails');
    }
  }

  /**
   * Sends an email from the specified account
   */
  async sendEmail({ email, to, subject, body, cc = [], bcc = [] }: SendEmailParams): Promise<SendEmailResponse> {
    try {
      const gmail = await this.getGmailClient(email);

      const message = [
        'Content-Type: text/plain; charset="UTF-8"\n',
        'MIME-Version: 1.0\n',
        'Content-Transfer-Encoding: 7bit\n',
        `To: ${to.join(', ')}\n`,
        cc.length > 0 ? `Cc: ${cc.join(', ')}\n` : '',
        bcc.length > 0 ? `Bcc: ${bcc.join(', ')}\n` : '',
        `Subject: ${subject}\n\n`,
        body,
      ].join('');

      const encodedMessage = Buffer.from(message)
        .toString('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/, '');

      const { data } = await gmail.users.messages.send({
        userId: 'me',
        requestBody: {
          raw: encodedMessage,
        },
      });

      return {
        messageId: data.id!,
        threadId: data.threadId!,
        labelIds: data.labelIds || undefined,
      };
    } catch (error) {
      throw this.handleError(error, 'Failed to send email');
    }
  }

  /**
   * Gets Gmail settings and profile information
   */
  async getWorkspaceGmailSettings({ email }: GetGmailSettingsParams): Promise<GetGmailSettingsResponse> {
    try {
      const gmail = await this.getGmailClient(email);

      const { data: profile } = await gmail.users.getProfile({
        userId: 'me'
      });

      const [
        { data: autoForwarding },
        { data: imap },
        { data: language },
        { data: pop },
        { data: vacation }
      ] = await Promise.all([
        gmail.users.settings.getAutoForwarding({ userId: 'me' }),
        gmail.users.settings.getImap({ userId: 'me' }),
        gmail.users.settings.getLanguage({ userId: 'me' }),
        gmail.users.settings.getPop({ userId: 'me' }),
        gmail.users.settings.getVacation({ userId: 'me' })
      ]);

      const nullSafeString = (value: string | null | undefined): string | undefined => 
        value === null ? undefined : value;

      return {
        profile: {
          emailAddress: profile.emailAddress ?? '',
          messagesTotal: typeof profile.messagesTotal === 'number' ? profile.messagesTotal : 0,
          threadsTotal: typeof profile.threadsTotal === 'number' ? profile.threadsTotal : 0,
          historyId: profile.historyId ?? ''
        },
        settings: {
          ...(language?.displayLanguage && {
            language: {
              displayLanguage: language.displayLanguage
            }
          }),
          ...(autoForwarding && {
            autoForwarding: {
              enabled: Boolean(autoForwarding.enabled),
              ...(autoForwarding.emailAddress && {
                emailAddress: autoForwarding.emailAddress
              })
            }
          }),
          ...(imap && {
            imap: {
              enabled: Boolean(imap.enabled),
              ...(typeof imap.autoExpunge === 'boolean' && {
                autoExpunge: imap.autoExpunge
              }),
              ...(imap.expungeBehavior && {
                expungeBehavior: imap.expungeBehavior
              })
            }
          }),
          ...(pop && {
            pop: {
              enabled: Boolean(pop.accessWindow),
              ...(pop.accessWindow && {
                accessWindow: pop.accessWindow
              })
            }
          }),
          ...(vacation && {
            vacationResponder: {
              enabled: Boolean(vacation.enableAutoReply),
              ...(vacation.startTime && {
                startTime: vacation.startTime
              }),
              ...(vacation.endTime && {
                endTime: vacation.endTime
              }),
              ...(vacation.responseSubject && {
                responseSubject: vacation.responseSubject
              }),
              ...((vacation.responseBodyHtml || vacation.responseBodyPlainText) && {
                message: vacation.responseBodyHtml ?? vacation.responseBodyPlainText ?? ''
              })
            }
          })
        }
      } as GetGmailSettingsResponse;
    } catch (error) {
      throw this.handleError(error, 'Failed to get Gmail settings');
    }
  }
}

```
Page 2/3FirstPrevNextLast