This is page 2 of 4. Use http://codebase.md/aaronsb/google-workspace-mcp?lines=true&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/services/base/BaseGoogleService.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { OAuth2Client } from 'google-auth-library';
  2 | import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
  3 | import { getAccountManager } from '../../modules/accounts/index.js';
  4 | 
  5 | /**
  6 |  * Base error class for Google services
  7 |  */
  8 | export class GoogleServiceError extends McpError {
  9 |   constructor(
 10 |     message: string,
 11 |     code: string,
 12 |     details?: string
 13 |   ) {
 14 |     super(ErrorCode.InternalError, message, { code, details });
 15 |   }
 16 | }
 17 | 
 18 | /**
 19 |  * Configuration interface for Google services
 20 |  */
 21 | export interface GoogleServiceConfig {
 22 |   serviceName: string;
 23 |   version: string;
 24 | }
 25 | 
 26 | /**
 27 |  * Base class for Google service implementations.
 28 |  * Provides common functionality for authentication, error handling, and client management.
 29 |  */
 30 | export abstract class BaseGoogleService<TClient> {
 31 |   protected oauth2Client?: OAuth2Client;
 32 |   private apiClients: Map<string, TClient> = new Map();
 33 |   private readonly serviceName: string;
 34 | 
 35 |   constructor(config: GoogleServiceConfig) {
 36 |     this.serviceName = config.serviceName;
 37 |   }
 38 | 
 39 |   /**
 40 |    * Initializes the service by setting up OAuth2 client
 41 |    */
 42 |   protected async initialize(): Promise<void> {
 43 |     try {
 44 |       const accountManager = getAccountManager();
 45 |       this.oauth2Client = await accountManager.getAuthClient();
 46 |     } catch (error) {
 47 |       throw this.handleError(error, 'Failed to initialize service');
 48 |     }
 49 |   }
 50 | 
 51 |   /**
 52 |    * Gets an authenticated API client for the service
 53 |    * 
 54 |    * @param email - The email address to get a client for
 55 |    * @param clientFactory - Function to create the specific Google API client
 56 |    * @returns The authenticated API client
 57 |    */
 58 |   protected async getAuthenticatedClient(
 59 |     email: string,
 60 |     clientFactory: (auth: OAuth2Client) => TClient
 61 |   ): Promise<TClient> {
 62 |     if (!this.oauth2Client) {
 63 |       throw new GoogleServiceError(
 64 |         `${this.serviceName} client not initialized`,
 65 |         'CLIENT_ERROR',
 66 |         'Please ensure the service is initialized'
 67 |       );
 68 |     }
 69 | 
 70 |     const existingClient = this.apiClients.get(email);
 71 |     if (existingClient) {
 72 |       return existingClient;
 73 |     }
 74 | 
 75 |     try {
 76 |       const accountManager = getAccountManager();
 77 |       const tokenStatus = await accountManager.validateToken(email);
 78 | 
 79 |       if (!tokenStatus.valid || !tokenStatus.token) {
 80 |         throw new GoogleServiceError(
 81 |           `${this.serviceName} authentication required`,
 82 |           'AUTH_REQUIRED',
 83 |           'Please authenticate the account'
 84 |         );
 85 |       }
 86 | 
 87 |       this.oauth2Client.setCredentials(tokenStatus.token);
 88 |       const client = clientFactory(this.oauth2Client);
 89 |       this.apiClients.set(email, client);
 90 |       return client;
 91 |     } catch (error) {
 92 |       throw this.handleError(error, 'Failed to get authenticated client');
 93 |     }
 94 |   }
 95 | 
 96 |   /**
 97 |    * Common error handler for Google service operations
 98 |    */
 99 |   protected handleError(error: unknown, context: string): GoogleServiceError {
100 |     if (error instanceof GoogleServiceError) {
101 |       return error;
102 |     }
103 | 
104 |     return new GoogleServiceError(
105 |       context,
106 |       'SERVICE_ERROR',
107 |       `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
108 |     );
109 |   }
110 | 
111 |   /**
112 |    * Validates required scopes are present for an operation
113 |    */
114 |   protected async validateScopes(email: string, requiredScopes: string[]): Promise<void> {
115 |     try {
116 |       const accountManager = getAccountManager();
117 |       const tokenInfo = await accountManager.validateToken(email);
118 | 
119 |       if (!tokenInfo.requiredScopes) {
120 |         throw new GoogleServiceError(
121 |           'No scopes found in token',
122 |           'SCOPE_ERROR',
123 |           'Token does not contain scope information'
124 |         );
125 |       }
126 | 
127 |       const missingScopes = requiredScopes.filter(
128 |         scope => !tokenInfo.requiredScopes?.includes(scope)
129 |       );
130 | 
131 |       if (missingScopes.length > 0) {
132 |         throw new GoogleServiceError(
133 |           'Insufficient permissions',
134 |           'SCOPE_ERROR',
135 |           `Missing required scopes: ${missingScopes.join(', ')}`
136 |         );
137 |       }
138 |     } catch (error) {
139 |       throw this.handleError(error, 'Failed to validate scopes');
140 |     }
141 |   }
142 | }
143 | 
```
--------------------------------------------------------------------------------
/docs/TOOL_DISCOVERY.md:
--------------------------------------------------------------------------------
```markdown
  1 | # Tool Discovery and Aliases
  2 | 
  3 | The Google Workspace MCP server provides several features to make tools more discoverable and easier to use:
  4 | 
  5 | ## Tool Categories
  6 | 
  7 | Tools are organized into logical categories with clear dependencies:
  8 | 
  9 | ### Account Management (Required First)
 10 | - Authentication and account management
 11 |   - list_workspace_accounts (foundation for all operations)
 12 |   - authenticate_workspace_account
 13 |   - remove_workspace_account
 14 | 
 15 | ### Gmail Management
 16 | - Messages
 17 |   - search_workspace_emails
 18 |   - send_workspace_email
 19 |   - get_workspace_gmail_settings
 20 |   - manage_workspace_draft
 21 | - Labels
 22 |   - manage_workspace_label
 23 |   - manage_workspace_label_assignment
 24 |   - manage_workspace_label_filter
 25 | 
 26 | ### Calendar Management
 27 | - Events
 28 |   - list_workspace_calendar_events
 29 |   - get_workspace_calendar_event
 30 |   - manage_workspace_calendar_event
 31 |   - create_workspace_calendar_event
 32 |   - delete_workspace_calendar_event
 33 | 
 34 | ### Drive Management
 35 | - Files
 36 |   - list_drive_files
 37 |   - search_drive_files
 38 |   - upload_drive_file
 39 |   - download_drive_file
 40 | - Folders
 41 |   - create_drive_folder
 42 | - Permissions
 43 |   - update_drive_permissions
 44 | - Operations
 45 |   - delete_drive_file
 46 | 
 47 | IMPORTANT: The list_workspace_accounts tool MUST be called before any other workspace operations to:
 48 | 1. Check for existing authenticated accounts
 49 | 2. Determine which account to use if multiple exist
 50 | 3. Verify required API scopes are authorized
 51 | 
 52 | ## Tool Aliases
 53 | 
 54 | Most tools support multiple aliases for more intuitive usage. For example:
 55 | 
 56 | ```javascript
 57 | // All of these are equivalent:
 58 | create_workspace_label
 59 | create_label
 60 | new_label
 61 | create_gmail_label
 62 | ```
 63 | 
 64 | ## Improved Error Messages
 65 | 
 66 | When a tool name is not found, the server provides helpful suggestions:
 67 | 
 68 | ```
 69 | Tool 'create_gmail_lable' not found.
 70 | 
 71 | Did you mean:
 72 | - create_workspace_label (Gmail/Labels)
 73 |   Aliases: create_label, new_label, create_gmail_label
 74 | 
 75 | Available categories:
 76 | - Gmail/Labels: create_label, update_label, delete_label
 77 | - Gmail/Messages: send_email, search_emails
 78 | - Calendar/Events: create_event, update_event, delete_event
 79 | ```
 80 | 
 81 | ## Tool Metadata
 82 | 
 83 | Each tool includes:
 84 | 
 85 | - Category: Logical grouping for organization
 86 | - Aliases: Alternative names for the tool
 87 | - Description: Detailed usage information
 88 | - Input Schema: Required and optional parameters
 89 | 
 90 | ## Best Practices
 91 | 
 92 | 1. Use the most specific tool name when possible
 93 | 2. Check error messages for similar tool suggestions
 94 | 3. Use the list_workspace_tools command to see all available tools
 95 | 4. Refer to tool categories for related functionality
 96 | 
 97 | ## Examples
 98 | 
 99 | ### Creating a Label
100 | 
101 | ```javascript
102 | // Any of these work:
103 | create_workspace_label({
104 |   email: "[email protected]",
105 |   name: "Important/Projects",
106 |   messageListVisibility: "show",
107 |   labelListVisibility: "labelShow",
108 |   color: {
109 |     textColor: "#000000",
110 |     backgroundColor: "#E7E7E7"
111 |   }
112 | })
113 | 
114 | create_label({
115 |   email: "[email protected]",
116 |   name: "Important/Projects"
117 | })
118 | ```
119 | 
120 | ### Creating a Label Filter
121 | 
122 | ```javascript
123 | // Create a filter to automatically label incoming emails
124 | create_workspace_label_filter({
125 |   email: "[email protected]",
126 |   labelId: "Label_123",
127 |   criteria: {
128 |     from: ["[email protected]"],
129 |     subject: "Project Update",
130 |     hasAttachment: true
131 |   },
132 |   actions: {
133 |     addLabel: true,
134 |     markImportant: true
135 |   }
136 | })
137 | ```
138 | 
139 | ### Managing Message Labels
140 | 
141 | ```javascript
142 | // Add/remove labels from a message
143 | modify_workspace_message_labels({
144 |   email: "[email protected]",
145 |   messageId: "msg_123",
146 |   addLabelIds: ["Label_123"],
147 |   removeLabelIds: ["UNREAD"]
148 | })
149 | ```
150 | 
151 | ### Sending an Email
152 | 
153 | ```javascript
154 | // These are equivalent:
155 | send_workspace_email({
156 |   email: "[email protected]",
157 |   to: ["[email protected]"],
158 |   subject: "Hello",
159 |   body: "Message content",
160 |   cc: ["[email protected]"]
161 | })
162 | 
163 | send_email({
164 |   email: "[email protected]",
165 |   to: ["[email protected]"],
166 |   subject: "Hello",
167 |   body: "Message content"
168 | })
169 | ```
170 | 
171 | ## Future Improvements
172 | 
173 | - Category descriptions and documentation
174 | - Tool relationship mapping
175 | - Common usage patterns and workflows
176 | - Interactive tool discovery
177 | - Workflow templates for common tasks
178 | - Error handling best practices
179 | - Performance optimization guidelines
180 | - Security and permission management
181 | 
```
--------------------------------------------------------------------------------
/src/services/contacts/index.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { google } from "googleapis";
  2 | import {
  3 |   BaseGoogleService,
  4 |   GoogleServiceError
  5 | } from "../base/BaseGoogleService.js";
  6 | import { McpError } from "@modelcontextprotocol/sdk/types.js";
  7 | import {
  8 |   GetContactsParams,
  9 |   GetContactsResponse,
 10 |   ContactsError
 11 | } from "../../modules/contacts/types.js";
 12 | import { CONTACTS_SCOPES } from "../../modules/contacts/scopes.js";
 13 | 
 14 | // Type alias for the Google People API client
 15 | type PeopleApiClient = ReturnType<typeof google.people>;
 16 | 
 17 | /**
 18 |  * Contacts service implementation extending BaseGoogleService.
 19 |  * Handles Google Contacts (People API) specific operations.
 20 |  */
 21 | export class ContactsService extends BaseGoogleService<PeopleApiClient> {
 22 |   constructor() {
 23 |     super({
 24 |       serviceName: "people", // Use 'people' for the People API
 25 |       version: "v1"
 26 |     });
 27 |     // Initialize immediately or ensure initialized before first use
 28 |     this.initialize();
 29 |   }
 30 | 
 31 |   /**
 32 |    * Gets an authenticated People API client for the specified account.
 33 |    */
 34 |   private async getPeopleClient(email: string): Promise<PeopleApiClient> {
 35 |     // The clientFactory function tells BaseGoogleService how to create the specific client
 36 |     return this.getAuthenticatedClient(email, (auth) =>
 37 |       google.people({ version: "v1", auth })
 38 |     );
 39 |   }
 40 | 
 41 |   /**
 42 |    * Retrieves contacts for the specified user account.
 43 |    */
 44 |   async getContacts(params: GetContactsParams): Promise<GetContactsResponse> {
 45 |     const { email, pageSize, pageToken, personFields } = params;
 46 | 
 47 |     if (!personFields) {
 48 |       throw new ContactsError(
 49 |         "Missing required parameter: personFields",
 50 |         "INVALID_PARAMS",
 51 |         'Specify the fields to retrieve (e.g. "names,emailAddresses")'
 52 |       );
 53 |     }
 54 | 
 55 |     try {
 56 |       // Ensure necessary scopes are granted
 57 |       await this.validateScopes(email, [CONTACTS_SCOPES.READONLY]);
 58 | 
 59 |       const peopleApi = await this.getPeopleClient(email);
 60 | 
 61 |       const response = await peopleApi.people.connections.list({
 62 |         resourceName: "people/me", // 'people/me' refers to the authenticated user
 63 |         pageSize: pageSize,
 64 |         pageToken: pageToken,
 65 |         personFields: personFields,
 66 |         // requestSyncToken: true // Consider adding for sync capabilities later
 67 |       });
 68 | 
 69 |       // We might want to add more robust mapping/validation here
 70 |       // For now we assume the response structure matches GetContactsResponse
 71 |       // Note: googleapis types might use 'null' where we defined optional fields ('undefined')
 72 |       // Need to handle potential nulls if strict null checks are enabled
 73 |       return response.data as GetContactsResponse;
 74 |     } catch (error) {
 75 |       // Handle known GoogleServiceError specifically
 76 |       if (error instanceof GoogleServiceError) {
 77 |         // Assuming GoogleServiceError inherits message and data from McpError
 78 |         // Use type assertion as the linter seems unsure
 79 |         const gError = error as McpError & {
 80 |           data?: { code?: string; details?: string };
 81 |         };
 82 |         throw new ContactsError(
 83 |           gError.message || "Error retrieving contacts", // Fallback message
 84 |           gError.data?.code || "GOOGLE_SERVICE_ERROR", // Code from data
 85 |           gError.data?.details // Details from data
 86 |         );
 87 |       }
 88 |       // Handle other potential errors (e.g. network errors)
 89 |       else if (error instanceof Error) {
 90 |         throw new ContactsError(
 91 |           `Failed to retrieve contacts: ${error.message}`,
 92 |           "UNKNOWN_API_ERROR" // More specific code
 93 |         );
 94 |       }
 95 |       // Handle non-Error throws
 96 |       else {
 97 |         throw new ContactsError(
 98 |           "Failed to retrieve contacts due to an unknown issue",
 99 |           "UNKNOWN_INTERNAL_ERROR" // More specific code
100 |         );
101 |       }
102 |     }
103 |   }
104 | 
105 |   // Add other methods like searchContacts, createContact, updateContact, deleteContact later
106 | }
107 | 
108 | // Optional: Export a singleton instance if needed by the module structure
109 | // let contactsServiceInstance: ContactsService | null = null;
110 | // export function getContactsService(): ContactsService {
111 | //   if (!contactsServiceInstance) {
112 | //     contactsServiceInstance = new ContactsService();
113 | //   }
114 | //   return contactsServiceInstance;
115 | // }
116 | 
```
--------------------------------------------------------------------------------
/src/api/request.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { google } from 'googleapis';
  2 | import { OAuth2Client } from 'google-auth-library';
  3 | import { ApiRequestParams, GoogleApiError } from '../types.js';
  4 | 
  5 | interface ServiceVersionMap {
  6 |   [key: string]: string;
  7 | }
  8 | 
  9 | export class GoogleApiRequest {
 10 |   private readonly serviceVersions: ServiceVersionMap = {
 11 |     gmail: 'v1',
 12 |     drive: 'v3',
 13 |     sheets: 'v4',
 14 |     calendar: 'v3'
 15 |   };
 16 | 
 17 |   constructor(private authClient: OAuth2Client) {}
 18 | 
 19 |   async makeRequest({
 20 |     endpoint,
 21 |     method,
 22 |     params = {},
 23 |     token
 24 |   }: ApiRequestParams): Promise<any> {
 25 |     try {
 26 |       // Set up authentication
 27 |       this.authClient.setCredentials({
 28 |         access_token: token
 29 |       });
 30 | 
 31 |       // Parse the endpoint to get service and method
 32 |       const [service, ...methodParts] = endpoint.split('.');
 33 |       const methodName = methodParts.join('.');
 34 | 
 35 |       // Get the Google API service
 36 |       const googleService = await this.getGoogleService(service);
 37 | 
 38 |       // Navigate to the method in the service
 39 |       const apiMethod = this.getApiMethod(googleService, methodName);
 40 | 
 41 |       // Make the API request with proper context binding
 42 |       const response = await apiMethod.call(googleService, {
 43 |         ...params,
 44 |         auth: this.authClient
 45 |       });
 46 | 
 47 |       return response.data;
 48 |     } catch (error: any) {
 49 |       throw this.handleApiError(error);
 50 |     }
 51 |   }
 52 | 
 53 |   private async getGoogleService(service: string): Promise<any> {
 54 |     const version = this.serviceVersions[service];
 55 |     if (!version) {
 56 |       throw new GoogleApiError(
 57 |         `Service "${service}" is not supported`,
 58 |         'SERVICE_NOT_SUPPORTED',
 59 |         `Supported services are: ${Object.keys(this.serviceVersions).join(', ')}`
 60 |       );
 61 |     }
 62 | 
 63 |     const serviceConstructor = (google as any)[service];
 64 |     if (!serviceConstructor) {
 65 |       throw new GoogleApiError(
 66 |         `Failed to initialize ${service} service`,
 67 |         'SERVICE_INIT_FAILED',
 68 |         'Please check the service name and try again'
 69 |       );
 70 |     }
 71 | 
 72 |     return serviceConstructor({ version, auth: this.authClient });
 73 |   }
 74 | 
 75 |   private getApiMethod(service: any, methodPath: string): (params: Record<string, any>) => Promise<any> {
 76 |     const method = methodPath.split('.').reduce((obj: any, part) => obj?.[part], service);
 77 | 
 78 |     if (typeof method !== 'function') {
 79 |       throw new GoogleApiError(
 80 |         `Method "${methodPath}" not found`,
 81 |         'METHOD_NOT_FOUND',
 82 |         'Please check the method name and try again'
 83 |       );
 84 |     }
 85 | 
 86 |     return method;
 87 |   }
 88 | 
 89 |   private handleApiError(error: any): never {
 90 |     if (error instanceof GoogleApiError) {
 91 |       throw error;
 92 |     }
 93 | 
 94 |     // Handle Google API specific errors
 95 |     if (error.response) {
 96 |       const { status, data } = error.response;
 97 |       const errorInfo = this.getErrorInfo(status, data);
 98 |       throw new GoogleApiError(
 99 |         errorInfo.message,
100 |         errorInfo.code,
101 |         errorInfo.resolution
102 |       );
103 |     }
104 | 
105 |     // Handle network or other errors
106 |     throw new GoogleApiError(
107 |       error.message || 'Unknown error occurred',
108 |       'API_REQUEST_ERROR',
109 |       'Check your network connection and try again'
110 |     );
111 |   }
112 | 
113 |   private getErrorInfo(status: number, data: any): { message: string; code: string; resolution: string } {
114 |     const errorMap: Record<number, { code: string; resolution: string }> = {
115 |       400: {
116 |         code: 'BAD_REQUEST',
117 |         resolution: 'Check the request parameters and try again'
118 |       },
119 |       401: {
120 |         code: 'UNAUTHORIZED',
121 |         resolution: 'Token may be expired. Try refreshing the token'
122 |       },
123 |       403: {
124 |         code: 'FORBIDDEN',
125 |         resolution: 'Insufficient permissions. Check required scopes'
126 |       },
127 |       404: {
128 |         code: 'NOT_FOUND',
129 |         resolution: 'Resource not found. Verify the endpoint and parameters'
130 |       },
131 |       429: {
132 |         code: 'RATE_LIMIT',
133 |         resolution: 'Rate limit exceeded. Try again later'
134 |       },
135 |       500: {
136 |         code: 'SERVER_ERROR',
137 |         resolution: 'Internal server error. Please try again later'
138 |       },
139 |       503: {
140 |         code: 'SERVICE_UNAVAILABLE',
141 |         resolution: 'Service is temporarily unavailable. Please try again later'
142 |       }
143 |     };
144 | 
145 |     const errorInfo = errorMap[status] || {
146 |       code: `API_ERROR_${status}`,
147 |       resolution: 'Check the API documentation for more details'
148 |     };
149 | 
150 |     return {
151 |       message: data.error?.message || 'API request failed',
152 |       code: errorInfo.code,
153 |       resolution: errorInfo.resolution
154 |     };
155 |   }
156 | }
157 | 
```
--------------------------------------------------------------------------------
/docs/EXAMPLES.md:
--------------------------------------------------------------------------------
```markdown
  1 | # Google Workspace MCP Examples
  2 | 
  3 | This document provides examples of using the Google Workspace MCP tools.
  4 | 
  5 | ## Account Management
  6 | 
  7 | ```typescript
  8 | // List configured accounts
  9 | const accounts = await mcp.callTool('list_workspace_accounts', {});
 10 | 
 11 | // Authenticate a new account
 12 | const auth = await mcp.callTool('authenticate_workspace_account', {
 13 |   email: '[email protected]'
 14 | });
 15 | 
 16 | // Remove an account
 17 | await mcp.callTool('remove_workspace_account', {
 18 |   email: '[email protected]'
 19 | });
 20 | ```
 21 | 
 22 | ## Gmail Operations
 23 | 
 24 | ### Messages
 25 | 
 26 | ```typescript
 27 | // Search emails
 28 | const emails = await mcp.callTool('search_workspace_emails', {
 29 |   email: '[email protected]',
 30 |   search: {
 31 |     from: '[email protected]',
 32 |     subject: 'Important Meeting',
 33 |     after: '2024-01-01',
 34 |     hasAttachment: true
 35 |   }
 36 | });
 37 | 
 38 | // Example response with simplified attachment format (v1.1)
 39 | {
 40 |   "emails": [{
 41 |     "id": "msg123",
 42 |     "subject": "Important Meeting",
 43 |     "from": "[email protected]",
 44 |     "hasAttachment": true,
 45 |     "attachments": [{
 46 |       "name": "presentation.pdf"
 47 |     }]
 48 |   }]
 49 | }
 50 | 
 51 | // Send email
 52 | await mcp.callTool('send_workspace_email', {
 53 |   email: '[email protected]',
 54 |   to: ['[email protected]'],
 55 |   subject: 'Hello',
 56 |   body: 'Message content'
 57 | });
 58 | ```
 59 | 
 60 | ### Labels
 61 | 
 62 | ```typescript
 63 | // Create label
 64 | await mcp.callTool('manage_workspace_label', {
 65 |   email: '[email protected]',
 66 |   action: 'create',
 67 |   data: {
 68 |     name: 'Projects/Active',
 69 |     labelListVisibility: 'labelShow',
 70 |     messageListVisibility: 'show'
 71 |   }
 72 | });
 73 | 
 74 | // Apply label to message
 75 | await mcp.callTool('manage_workspace_label_assignment', {
 76 |   email: '[email protected]',
 77 |   action: 'add',
 78 |   messageId: 'msg123',
 79 |   labelIds: ['label123']
 80 | });
 81 | ```
 82 | 
 83 | ### Drafts
 84 | 
 85 | ```typescript
 86 | // Create draft
 87 | const draft = await mcp.callTool('manage_workspace_draft', {
 88 |   email: '[email protected]',
 89 |   action: 'create',
 90 |   data: {
 91 |     to: ['[email protected]'],
 92 |     subject: 'Draft Message',
 93 |     body: 'Draft content'
 94 |   }
 95 | });
 96 | 
 97 | // Send draft
 98 | await mcp.callTool('manage_workspace_draft', {
 99 |   email: '[email protected]',
100 |   action: 'send',
101 |   draftId: draft.id
102 | });
103 | ```
104 | 
105 | ## Calendar Operations
106 | 
107 | ### Events
108 | 
109 | ```typescript
110 | // List calendar events
111 | const events = await mcp.callTool('list_workspace_calendar_events', {
112 |   email: '[email protected]',
113 |   timeMin: '2024-02-01T00:00:00Z',
114 |   timeMax: '2024-02-28T23:59:59Z'
115 | });
116 | 
117 | // Create event
118 | await mcp.callTool('create_workspace_calendar_event', {
119 |   email: '[email protected]',
120 |   summary: 'Team Meeting',
121 |   start: {
122 |     dateTime: '2024-02-20T10:00:00-06:00',
123 |     timeZone: 'America/Chicago'
124 |   },
125 |   end: {
126 |     dateTime: '2024-02-20T11:00:00-06:00',
127 |     timeZone: 'America/Chicago'
128 |   },
129 |   attendees: [
130 |     { email: '[email protected]' }
131 |   ]
132 | });
133 | 
134 | // Respond to event
135 | await mcp.callTool('manage_workspace_calendar_event', {
136 |   email: '[email protected]',
137 |   eventId: 'evt123',
138 |   action: 'accept',
139 |   comment: 'Looking forward to it!'
140 | });
141 | ```
142 | 
143 | ## Drive Operations
144 | 
145 | ### File Management
146 | 
147 | ```typescript
148 | // List files
149 | const files = await mcp.callTool('list_drive_files', {
150 |   email: '[email protected]',
151 |   options: {
152 |     folderId: 'folder123',
153 |     pageSize: 100
154 |   }
155 | });
156 | 
157 | // Search files
158 | const searchResults = await mcp.callTool('search_drive_files', {
159 |   email: '[email protected]',
160 |   options: {
161 |     fullText: 'project proposal',
162 |     mimeType: 'application/pdf'
163 |   }
164 | });
165 | 
166 | // Upload file
167 | const uploadedFile = await mcp.callTool('upload_drive_file', {
168 |   email: '[email protected]',
169 |   options: {
170 |     name: 'document.pdf',
171 |     content: 'base64_encoded_content',
172 |     mimeType: 'application/pdf',
173 |     parents: ['folder123']
174 |   }
175 | });
176 | 
177 | // Download file
178 | const fileContent = await mcp.callTool('download_drive_file', {
179 |   email: '[email protected]',
180 |   fileId: 'file123',
181 |   mimeType: 'application/pdf'  // For Google Workspace files
182 | });
183 | 
184 | // Delete file
185 | await mcp.callTool('delete_drive_file', {
186 |   email: '[email protected]',
187 |   fileId: 'file123'
188 | });
189 | ```
190 | 
191 | ### Folder Operations
192 | 
193 | ```typescript
194 | // Create folder
195 | const folder = await mcp.callTool('create_drive_folder', {
196 |   email: '[email protected]',
197 |   name: 'Project Documents',
198 |   parentId: 'parent123'  // Optional
199 | });
200 | ```
201 | 
202 | ### Permissions
203 | 
204 | ```typescript
205 | // Update file permissions
206 | await mcp.callTool('update_drive_permissions', {
207 |   email: '[email protected]',
208 |   options: {
209 |     fileId: 'file123',
210 |     role: 'writer',
211 |     type: 'user',
212 |     emailAddress: '[email protected]'
213 |   }
214 | });
215 | 
216 | // Share with domain
217 | await mcp.callTool('update_drive_permissions', {
218 |   email: '[email protected]',
219 |   options: {
220 |     fileId: 'file123',
221 |     role: 'reader',
222 |     type: 'domain',
223 |     domain: 'example.com'
224 |   }
225 | });
226 | 
```
--------------------------------------------------------------------------------
/src/modules/tools/__tests__/registry.test.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { ToolRegistry, ToolMetadata } from '../registry.js';
  2 | 
  3 | describe('ToolRegistry', () => {
  4 |   const mockTools: ToolMetadata[] = [
  5 |     {
  6 |       name: 'create_workspace_label',
  7 |       category: 'Gmail/Labels',
  8 |       description: 'Create a new label',
  9 |       aliases: ['create_label', 'new_label', 'create_gmail_label'],
 10 |       inputSchema: {
 11 |         type: 'object',
 12 |         properties: {
 13 |           name: { type: 'string' }
 14 |         }
 15 |       }
 16 |     },
 17 |     {
 18 |       name: 'send_workspace_email',
 19 |       category: 'Gmail/Messages',
 20 |       description: 'Send an email',
 21 |       aliases: ['send_email', 'send_mail'],
 22 |       inputSchema: {
 23 |         type: 'object',
 24 |         properties: {
 25 |           to: { type: 'string' }
 26 |         }
 27 |       }
 28 |     },
 29 |     {
 30 |       name: 'create_workspace_calendar_event',
 31 |       category: 'Calendar/Events',
 32 |       description: 'Create calendar event',
 33 |       aliases: ['create_event', 'schedule_event'],
 34 |       inputSchema: {
 35 |         type: 'object',
 36 |         properties: {
 37 |           title: { type: 'string' }
 38 |         }
 39 |       }
 40 |     }
 41 |   ];
 42 | 
 43 |   let registry: ToolRegistry;
 44 | 
 45 |   beforeEach(() => {
 46 |     registry = new ToolRegistry(mockTools);
 47 |   });
 48 | 
 49 |   describe('getTool', () => {
 50 |     it('should find tool by main name', () => {
 51 |       const tool = registry.getTool('create_workspace_label');
 52 |       expect(tool).toBeDefined();
 53 |       expect(tool?.name).toBe('create_workspace_label');
 54 |     });
 55 | 
 56 |     it('should find tool by alias', () => {
 57 |       const tool = registry.getTool('create_gmail_label');
 58 |       expect(tool).toBeDefined();
 59 |       expect(tool?.name).toBe('create_workspace_label');
 60 |     });
 61 | 
 62 |     it('should return undefined for unknown tool', () => {
 63 |       const tool = registry.getTool('nonexistent_tool');
 64 |       expect(tool).toBeUndefined();
 65 |     });
 66 |   });
 67 | 
 68 |   describe('getAllTools', () => {
 69 |     it('should return all registered tools', () => {
 70 |       const tools = registry.getAllTools();
 71 |       expect(tools).toHaveLength(3);
 72 |       expect(tools.map(t => t.name)).toContain('create_workspace_label');
 73 |       expect(tools.map(t => t.name)).toContain('send_workspace_email');
 74 |       expect(tools.map(t => t.name)).toContain('create_workspace_calendar_event');
 75 |     });
 76 |   });
 77 | 
 78 |   describe('getCategories', () => {
 79 |     it('should return tools organized by category', () => {
 80 |       const categories = registry.getCategories();
 81 |       expect(categories).toHaveLength(3);
 82 |       
 83 |       const categoryNames = categories.map(c => c.name);
 84 |       expect(categoryNames).toContain('Gmail/Labels');
 85 |       expect(categoryNames).toContain('Gmail/Messages');
 86 |       expect(categoryNames).toContain('Calendar/Events');
 87 | 
 88 |       const labelCategory = categories.find(c => c.name === 'Gmail/Labels');
 89 |       expect(labelCategory?.tools).toHaveLength(1);
 90 |       expect(labelCategory?.tools[0].name).toBe('create_workspace_label');
 91 |     });
 92 |   });
 93 | 
 94 |   describe('findSimilarTools', () => {
 95 |     it('should find similar tools by name', () => {
 96 |       const similar = registry.findSimilarTools('create_label_workspace');
 97 |       expect(similar).toHaveLength(1);
 98 |       expect(similar[0].name).toBe('create_workspace_label');
 99 |     });
100 | 
101 |     it('should find similar tools by alias', () => {
102 |       const similar = registry.findSimilarTools('gmail_label_create');
103 |       expect(similar).toHaveLength(1);
104 |       expect(similar[0].name).toBe('create_workspace_label');
105 |     });
106 | 
107 |     it('should respect maxSuggestions limit', () => {
108 |       const similar = registry.findSimilarTools('create', 2);
109 |       expect(similar).toHaveLength(2);
110 |     });
111 |   });
112 | 
113 |   describe('formatErrorWithSuggestions', () => {
114 |     it('should format error message with suggestions', () => {
115 |       const message = registry.formatErrorWithSuggestions('create_gmail_lable');
116 |       expect(message).toContain('Tool \'create_gmail_lable\' not found');
117 |       expect(message).toContain('Did you mean:');
118 |       expect(message).toContain('create_workspace_label (Gmail/Labels)');
119 |       expect(message).toContain('Available categories:');
120 |     });
121 | 
122 |     it('should include aliases in error message', () => {
123 |       const message = registry.formatErrorWithSuggestions('send_mail_workspace');
124 |       expect(message).toContain('send_workspace_email');
125 |       expect(message).toContain('Aliases: send_email, send_mail');
126 |     });
127 |   });
128 | 
129 |   describe('getAllToolNames', () => {
130 |     it('should return all tool names including aliases', () => {
131 |       const names = registry.getAllToolNames();
132 |       expect(names).toContain('create_workspace_label');
133 |       expect(names).toContain('create_gmail_label');
134 |       expect(names).toContain('send_workspace_email');
135 |       expect(names).toContain('send_mail');
136 |     });
137 |   });
138 | });
139 | 
```
--------------------------------------------------------------------------------
/src/utils/token.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import fs from 'fs/promises';
  2 | import path from 'path';
  3 | import { TokenData, GoogleApiError, Account } from '../types.js';
  4 | 
  5 | const ENV_PREFIX = 'GOOGLE_TOKEN_';
  6 | 
  7 | export class TokenManager {
  8 |   private readonly credentialsDir: string;
  9 |   private readonly envTokens: Map<string, TokenData>;
 10 | 
 11 |   constructor() {
 12 |     this.credentialsDir = process.env.CREDENTIALS_DIR || path.resolve('config', 'credentials');
 13 |     this.envTokens = new Map();
 14 |     this.loadEnvTokens();
 15 |   }
 16 | 
 17 |   private loadEnvTokens(): void {
 18 |     for (const [key, value] of Object.entries(process.env)) {
 19 |       if (key.startsWith(ENV_PREFIX) && value) {
 20 |         try {
 21 |           const email = key
 22 |             .slice(ENV_PREFIX.length)
 23 |             .toLowerCase()
 24 |             .replace(/_/g, '.');
 25 |           const tokenData = JSON.parse(
 26 |             Buffer.from(value, 'base64').toString()
 27 |           ) as TokenData;
 28 |           this.envTokens.set(email, tokenData);
 29 |         } catch (error) {
 30 |           console.warn(`Failed to parse token from env var ${key}:`, error);
 31 |         }
 32 |       }
 33 |     }
 34 |   }
 35 | 
 36 |   private getTokenPath(email: string): string {
 37 |     const sanitizedEmail = email.replace(/[@.]/g, '-');
 38 |     return path.join(this.credentialsDir, `${sanitizedEmail}.token.json`);
 39 |   }
 40 | 
 41 |   private updateEnvToken(email: string, tokenData: TokenData): void {
 42 |     const safeEmail = email.replace(/[@.]/g, '_').toUpperCase();
 43 |     process.env[`${ENV_PREFIX}${safeEmail}`] = Buffer.from(
 44 |       JSON.stringify(tokenData)
 45 |     ).toString('base64');
 46 |     this.envTokens.set(email, tokenData);
 47 |   }
 48 | 
 49 |   async saveToken(email: string, tokenData: TokenData): Promise<void> {
 50 |     try {
 51 |       // Save to file system
 52 |       const tokenPath = this.getTokenPath(email);
 53 |       await fs.writeFile(tokenPath, JSON.stringify(tokenData, null, 2));
 54 |       
 55 |       // Update environment variable
 56 |       this.updateEnvToken(email, tokenData);
 57 |     } catch (error) {
 58 |       throw new GoogleApiError(
 59 |         'Failed to save token',
 60 |         'TOKEN_SAVE_ERROR',
 61 |         'Please ensure the directory specified by CREDENTIALS_DIR is writable'
 62 |       );
 63 |     }
 64 |   }
 65 | 
 66 |   async loadToken(email: string): Promise<TokenData | null> {
 67 |     // First try to load from environment variables
 68 |     const envToken = this.envTokens.get(email);
 69 |     if (envToken) {
 70 |       return envToken;
 71 |     }
 72 | 
 73 |     // Fall back to file system
 74 |     try {
 75 |       const tokenPath = this.getTokenPath(email);
 76 |       const data = await fs.readFile(tokenPath, 'utf-8');
 77 |       const token = JSON.parse(data) as TokenData;
 78 |       
 79 |       // Update environment variable for future use
 80 |       this.updateEnvToken(email, token);
 81 |       
 82 |       return token;
 83 |     } catch (error) {
 84 |       if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
 85 |         return null;
 86 |       }
 87 |       throw new GoogleApiError(
 88 |         'Failed to load token',
 89 |         'TOKEN_LOAD_ERROR',
 90 |         'Token file may be corrupted or inaccessible'
 91 |       );
 92 |     }
 93 |   }
 94 | 
 95 |   async deleteToken(email: string): Promise<void> {
 96 |     try {
 97 |       // Remove from file system
 98 |       const tokenPath = this.getTokenPath(email);
 99 |       await fs.unlink(tokenPath);
100 |       
101 |       // Remove from environment
102 |       const safeEmail = email.replace(/[@.]/g, '_').toUpperCase();
103 |       delete process.env[`${ENV_PREFIX}${safeEmail}`];
104 |       this.envTokens.delete(email);
105 |     } catch (error) {
106 |       if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
107 |         throw new GoogleApiError(
108 |           'Failed to delete token',
109 |           'TOKEN_DELETE_ERROR',
110 |           'Please ensure you have permission to delete the token file in CREDENTIALS_DIR'
111 |         );
112 |       }
113 |     }
114 |   }
115 | 
116 |   isTokenExpired(tokenData: TokenData): boolean {
117 |     // Consider token expired 5 minutes before actual expiry
118 |     const expiryBuffer = 5 * 60 * 1000; // 5 minutes in milliseconds
119 |     return Date.now() >= tokenData.expiry_date - expiryBuffer;
120 |   }
121 | 
122 |   hasRequiredScopes(tokenData: TokenData, requiredScopes: string[]): boolean {
123 |     const tokenScopes = new Set(tokenData.scope.split(' '));
124 |     return requiredScopes.every(scope => tokenScopes.has(scope));
125 |   }
126 | 
127 |   async getTokenStatus(email: string): Promise<Account['auth_status']> {
128 |     const token = await this.loadToken(email);
129 |     if (!token) {
130 |       return { has_token: false };
131 |     }
132 | 
133 |     return {
134 |       has_token: true,
135 |       scopes: token.scope.split(' '),
136 |       expires: token.expiry_date
137 |     };
138 |   }
139 | 
140 |   async validateToken(email: string, requiredScopes: string[]): Promise<{
141 |     valid: boolean;
142 |     token?: TokenData;
143 |     reason?: string;
144 |   }> {
145 |     const token = await this.loadToken(email);
146 |     
147 |     if (!token) {
148 |       return { valid: false, reason: 'Token not found' };
149 |     }
150 | 
151 |     if (this.isTokenExpired(token)) {
152 |       return { valid: false, token, reason: 'Token expired' };
153 |     }
154 | 
155 |     if (!this.hasRequiredScopes(token, requiredScopes)) {
156 |       return { valid: false, reason: 'Insufficient scopes' };
157 |     }
158 | 
159 |     return { valid: true, token };
160 |   }
161 | }
162 | 
```
--------------------------------------------------------------------------------
/src/services/calendar/index.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { google } from 'googleapis';
  2 | import { BaseGoogleService, GoogleServiceError } from '../base/BaseGoogleService.js';
  3 | import {
  4 |   GetEventsParams,
  5 |   CreateEventParams,
  6 |   EventResponse,
  7 |   CreateEventResponse,
  8 |   CalendarModuleConfig
  9 | } from '../../modules/calendar/types.js';
 10 | 
 11 | /**
 12 |  * Google Calendar Service Implementation extending BaseGoogleService.
 13 |  * Provides calendar functionality while leveraging common Google API patterns.
 14 |  */
 15 | export class CalendarService extends BaseGoogleService<ReturnType<typeof google.calendar>> {
 16 |   constructor(config?: CalendarModuleConfig) {
 17 |     super({
 18 |       serviceName: 'calendar',
 19 |       version: 'v3'
 20 |     });
 21 |   }
 22 | 
 23 |   /**
 24 |    * Get an authenticated Calendar client
 25 |    */
 26 |   private async getCalendarClient(email: string) {
 27 |     return this.getAuthenticatedClient(
 28 |       email,
 29 |       (auth) => google.calendar({ version: 'v3', auth })
 30 |     );
 31 |   }
 32 | 
 33 |   /**
 34 |    * Retrieve calendar events with optional filtering
 35 |    */
 36 |   async getEvents({ email, query, maxResults = 10, timeMin, timeMax }: GetEventsParams): Promise<EventResponse[]> {
 37 |     try {
 38 |       const calendar = await this.getCalendarClient(email);
 39 | 
 40 |       // Prepare search parameters
 41 |       const params: any = {
 42 |         calendarId: 'primary',
 43 |         maxResults,
 44 |         singleEvents: true,
 45 |         orderBy: 'startTime'
 46 |       };
 47 | 
 48 |       if (query) {
 49 |         params.q = query;
 50 |       }
 51 | 
 52 |       if (timeMin) {
 53 |         params.timeMin = new Date(timeMin).toISOString();
 54 |       }
 55 | 
 56 |       if (timeMax) {
 57 |         params.timeMax = new Date(timeMax).toISOString();
 58 |       }
 59 | 
 60 |       // List events matching criteria
 61 |       const { data } = await calendar.events.list(params);
 62 | 
 63 |       if (!data.items || data.items.length === 0) {
 64 |         return [];
 65 |       }
 66 | 
 67 |       // Map response to our EventResponse type
 68 |       return data.items.map(event => ({
 69 |         id: event.id!,
 70 |         summary: event.summary || '',
 71 |         description: event.description || undefined,
 72 |         start: {
 73 |           dateTime: event.start?.dateTime || event.start?.date || '',
 74 |           timeZone: event.start?.timeZone || 'UTC'
 75 |         },
 76 |         end: {
 77 |           dateTime: event.end?.dateTime || event.end?.date || '',
 78 |           timeZone: event.end?.timeZone || 'UTC'
 79 |         },
 80 |         attendees: event.attendees?.map(attendee => ({
 81 |           email: attendee.email!,
 82 |           responseStatus: attendee.responseStatus || undefined
 83 |         })),
 84 |         organizer: event.organizer ? {
 85 |           email: event.organizer.email!,
 86 |           self: event.organizer.self || false
 87 |         } : undefined
 88 |       }));
 89 |     } catch (error) {
 90 |       throw this.handleError(error, 'Failed to get events');
 91 |     }
 92 |   }
 93 | 
 94 |   /**
 95 |    * Retrieve a single calendar event by ID
 96 |    */
 97 |   async getEvent(email: string, eventId: string): Promise<EventResponse> {
 98 |     try {
 99 |       const calendar = await this.getCalendarClient(email);
100 | 
101 |       const { data: event } = await calendar.events.get({
102 |         calendarId: 'primary',
103 |         eventId
104 |       });
105 | 
106 |       if (!event) {
107 |         throw new GoogleServiceError(
108 |           'Event not found',
109 |           'NOT_FOUND',
110 |           `No event found with ID: ${eventId}`
111 |         );
112 |       }
113 | 
114 |       return {
115 |         id: event.id!,
116 |         summary: event.summary || '',
117 |         description: event.description || undefined,
118 |         start: {
119 |           dateTime: event.start?.dateTime || event.start?.date || '',
120 |           timeZone: event.start?.timeZone || 'UTC'
121 |         },
122 |         end: {
123 |           dateTime: event.end?.dateTime || event.end?.date || '',
124 |           timeZone: event.end?.timeZone || 'UTC'
125 |         },
126 |         attendees: event.attendees?.map(attendee => ({
127 |           email: attendee.email!,
128 |           responseStatus: attendee.responseStatus || undefined
129 |         })),
130 |         organizer: event.organizer ? {
131 |           email: event.organizer.email!,
132 |           self: event.organizer.self || false
133 |         } : undefined
134 |       };
135 |     } catch (error) {
136 |       throw this.handleError(error, 'Failed to get event');
137 |     }
138 |   }
139 | 
140 |   /**
141 |    * Create a new calendar event
142 |    */
143 |   async createEvent({ email, summary, description, start, end, attendees }: CreateEventParams): Promise<CreateEventResponse> {
144 |     try {
145 |       const calendar = await this.getCalendarClient(email);
146 | 
147 |       const eventData = {
148 |         summary,
149 |         description,
150 |         start,
151 |         end,
152 |         attendees: attendees?.map(attendee => ({ email: attendee.email }))
153 |       };
154 | 
155 |       const { data: event } = await calendar.events.insert({
156 |         calendarId: 'primary',
157 |         requestBody: eventData,
158 |         sendUpdates: 'all'  // Send emails to attendees
159 |       });
160 | 
161 |       if (!event.id || !event.summary) {
162 |         throw new GoogleServiceError(
163 |           'Failed to create event',
164 |           'CREATE_ERROR',
165 |           'Event creation response was incomplete'
166 |         );
167 |       }
168 | 
169 |       return {
170 |         id: event.id,
171 |         summary: event.summary,
172 |         htmlLink: event.htmlLink || ''
173 |       };
174 |     } catch (error) {
175 |       throw this.handleError(error, 'Failed to create event');
176 |     }
177 |   }
178 | }
179 | 
```
--------------------------------------------------------------------------------
/src/modules/gmail/services/base.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { google } from 'googleapis';
  2 | import { BaseGoogleService } from '../../../services/base/BaseGoogleService.js';
  3 | import {
  4 |   GetEmailsParams,
  5 |   SendEmailParams,
  6 |   SendEmailResponse,
  7 |   GetGmailSettingsParams,
  8 |   GetGmailSettingsResponse,
  9 |   GmailError,
 10 |   GmailModuleConfig,
 11 |   GetEmailsResponse,
 12 |   DraftResponse,
 13 |   GetDraftsResponse,
 14 |   Label,
 15 |   GetLabelsResponse,
 16 |   GetLabelFiltersResponse,
 17 |   LabelFilter
 18 | } from '../types.js';
 19 | import { AttachmentIndexService } from '../../attachments/index-service.js';
 20 | 
 21 | import {
 22 |   ManageLabelParams,
 23 |   ManageLabelAssignmentParams,
 24 |   ManageLabelFilterParams
 25 | } from './label.js';
 26 | import { ManageDraftParams } from './draft.js';
 27 | import { EmailService } from './email.js';
 28 | import { SearchService } from './search.js';
 29 | import { DraftService } from './draft.js';
 30 | import { SettingsService } from './settings.js';
 31 | import { LabelService } from './label.js';
 32 | import { GmailAttachmentService } from './attachment.js';
 33 | 
 34 | /**
 35 |  * Gmail service implementation extending BaseGoogleService for common auth handling.
 36 |  */
 37 | export class GmailService extends BaseGoogleService<ReturnType<typeof google.gmail>> {
 38 |   private emailService: EmailService;
 39 |   private searchService: SearchService;
 40 |   private draftService: DraftService;
 41 |   private settingsService: SettingsService;
 42 |   private labelService: LabelService;
 43 |   private attachmentService: GmailAttachmentService;
 44 |   private initialized = false;
 45 |   
 46 |   constructor(config?: GmailModuleConfig) {
 47 |     super({ serviceName: 'Gmail', version: 'v1' });
 48 |     
 49 |     this.searchService = new SearchService();
 50 |     this.attachmentService = GmailAttachmentService.getInstance();
 51 |     this.emailService = new EmailService(this.searchService, this.attachmentService);
 52 |     this.draftService = new DraftService(this.attachmentService);
 53 |     this.settingsService = new SettingsService();
 54 |     this.labelService = new LabelService();
 55 |   }
 56 | 
 57 |   private async ensureInitialized(email: string) {
 58 |     if (!this.initialized) {
 59 |       await this.initialize();
 60 |     }
 61 |     await this.getGmailClient(email);
 62 |   }
 63 | 
 64 |   /**
 65 |    * Initialize the Gmail service
 66 |    */
 67 |   public async initialize(): Promise<void> {
 68 |     try {
 69 |       await super.initialize();
 70 |       this.initialized = true;
 71 |     } catch (error) {
 72 |       throw new GmailError(
 73 |         'Failed to initialize Gmail service',
 74 |         'INIT_ERROR',
 75 |         error instanceof Error ? error.message : 'Unknown error'
 76 |       );
 77 |     }
 78 |   }
 79 | 
 80 |   /**
 81 |    * Ensures all services are properly initialized
 82 |    */
 83 |   private checkInitialized() {
 84 |     if (!this.initialized) {
 85 |       throw new GmailError(
 86 |         'Gmail service not initialized',
 87 |         'INIT_ERROR',
 88 |         'Please call init() before using the service'
 89 |       );
 90 |     }
 91 |   }
 92 | 
 93 |   /**
 94 |    * Gets an authenticated Gmail client for the specified account.
 95 |    */
 96 |   private async getGmailClient(email: string) {
 97 |     if (!this.initialized) {
 98 |       await this.initialize();
 99 |     }
100 |     
101 |     return this.getAuthenticatedClient(
102 |       email,
103 |       (auth) => {
104 |         const client = google.gmail({ version: 'v1', auth });
105 |         
106 |         // Update service instances with new client
107 |         this.emailService.updateClient(client);
108 |         this.draftService.updateClient(client);
109 |         this.settingsService.updateClient(client);
110 |         this.labelService.updateClient(client);
111 |         this.attachmentService.updateClient(client);
112 |         
113 |         return client;
114 |       }
115 |     );
116 |   }
117 | 
118 |   async getEmails(params: GetEmailsParams): Promise<GetEmailsResponse> {
119 |     await this.getGmailClient(params.email);
120 |     return this.emailService.getEmails(params);
121 |   }
122 | 
123 |   async sendEmail(params: SendEmailParams): Promise<SendEmailResponse> {
124 |     await this.getGmailClient(params.email);
125 |     return this.emailService.sendEmail(params);
126 |   }
127 | 
128 |   async manageDraft(params: ManageDraftParams): Promise<DraftResponse | GetDraftsResponse | SendEmailResponse | void> {
129 |     await this.getGmailClient(params.email);
130 |     return this.draftService.manageDraft(params);
131 |   }
132 | 
133 |   async getWorkspaceGmailSettings(params: GetGmailSettingsParams): Promise<GetGmailSettingsResponse> {
134 |     await this.getGmailClient(params.email);
135 |     return this.settingsService.getWorkspaceGmailSettings(params);
136 |   }
137 | 
138 |   // Consolidated Label Management Methods
139 |   async manageLabel(params: ManageLabelParams): Promise<Label | GetLabelsResponse | void> {
140 |     await this.getGmailClient(params.email);
141 |     return this.labelService.manageLabel(params);
142 |   }
143 | 
144 |   async manageLabelAssignment(params: ManageLabelAssignmentParams): Promise<void> {
145 |     await this.getGmailClient(params.email);
146 |     return this.labelService.manageLabelAssignment(params);
147 |   }
148 | 
149 |   async manageLabelFilter(params: ManageLabelFilterParams): Promise<LabelFilter | GetLabelFiltersResponse | void> {
150 |     await this.getGmailClient(params.email);
151 |     return this.labelService.manageLabelFilter(params);
152 |   }
153 | 
154 |   async getAttachment(email: string, messageId: string, filename: string) {
155 |     await this.ensureInitialized(email);
156 |     return this.attachmentService.getAttachment(email, messageId, filename);
157 |   }
158 | }
159 | 
```
--------------------------------------------------------------------------------
/src/api/validators/endpoint.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { GoogleApiError } from '../../types.js';
  2 | 
  3 | interface ServiceConfig {
  4 |   version: string;
  5 |   methods: string[];
  6 |   scopes: Record<string, string[]>;
  7 | }
  8 | 
  9 | export class EndpointValidator {
 10 |   // Registry of supported services and their configurations
 11 |   private readonly serviceRegistry: Record<string, ServiceConfig> = {
 12 |     calendar: {
 13 |       version: 'v3',
 14 |       methods: [
 15 |         'events.list',
 16 |         'events.get',
 17 |         'events.insert',
 18 |         'events.update',
 19 |         'events.delete',
 20 |         'events.attachments.get',
 21 |         'events.attachments.upload',
 22 |         'events.attachments.delete'
 23 |       ],
 24 |       scopes: {
 25 |         'events.list': ['https://www.googleapis.com/auth/calendar.readonly'],
 26 |         'events.get': ['https://www.googleapis.com/auth/calendar.readonly'],
 27 |         'events.insert': ['https://www.googleapis.com/auth/calendar.events'],
 28 |         'events.update': ['https://www.googleapis.com/auth/calendar.events'],
 29 |         'events.delete': ['https://www.googleapis.com/auth/calendar.events'],
 30 |         'events.attachments.get': ['https://www.googleapis.com/auth/calendar.readonly'],
 31 |         'events.attachments.upload': ['https://www.googleapis.com/auth/calendar.events'],
 32 |         'events.attachments.delete': ['https://www.googleapis.com/auth/calendar.events']
 33 |       }
 34 |     },
 35 |     gmail: {
 36 |       version: 'v1',
 37 |       methods: [
 38 |         'users.messages.list',
 39 |         'users.messages.get',
 40 |         'users.messages.send',
 41 |         'users.labels.list',
 42 |         'users.labels.get',
 43 |         'users.drafts.list',
 44 |         'users.drafts.get',
 45 |         'users.drafts.create',
 46 |         'users.drafts.update',
 47 |         'users.drafts.delete'
 48 |       ],
 49 |       scopes: {
 50 |         'users.messages.list': ['https://www.googleapis.com/auth/gmail.readonly'],
 51 |         'users.messages.get': ['https://www.googleapis.com/auth/gmail.readonly'],
 52 |         'users.messages.send': ['https://www.googleapis.com/auth/gmail.send'],
 53 |         'users.labels.list': ['https://www.googleapis.com/auth/gmail.labels'],
 54 |         'users.labels.get': ['https://www.googleapis.com/auth/gmail.labels'],
 55 |         'users.drafts.list': ['https://www.googleapis.com/auth/gmail.readonly'],
 56 |         'users.drafts.get': ['https://www.googleapis.com/auth/gmail.readonly'],
 57 |         'users.drafts.create': ['https://www.googleapis.com/auth/gmail.compose'],
 58 |         'users.drafts.update': ['https://www.googleapis.com/auth/gmail.compose'],
 59 |         'users.drafts.delete': ['https://www.googleapis.com/auth/gmail.compose']
 60 |       }
 61 |     },
 62 |     drive: {
 63 |       version: 'v3',
 64 |       methods: [
 65 |         'files.list',
 66 |         'files.get',
 67 |         'files.create',
 68 |         'files.update',
 69 |         'files.delete',
 70 |         'files.copy',
 71 |         'permissions.list',
 72 |         'permissions.get',
 73 |         'permissions.create',
 74 |         'permissions.update',
 75 |         'permissions.delete'
 76 |       ],
 77 |       scopes: {
 78 |         'files.list': ['https://www.googleapis.com/auth/drive.readonly'],
 79 |         'files.get': ['https://www.googleapis.com/auth/drive.readonly'],
 80 |         'files.create': ['https://www.googleapis.com/auth/drive.file'],
 81 |         'files.update': ['https://www.googleapis.com/auth/drive.file'],
 82 |         'files.delete': ['https://www.googleapis.com/auth/drive.file'],
 83 |         'files.copy': ['https://www.googleapis.com/auth/drive.file'],
 84 |         'permissions.list': ['https://www.googleapis.com/auth/drive.readonly'],
 85 |         'permissions.get': ['https://www.googleapis.com/auth/drive.readonly'],
 86 |         'permissions.create': ['https://www.googleapis.com/auth/drive.file'],
 87 |         'permissions.update': ['https://www.googleapis.com/auth/drive.file'],
 88 |         'permissions.delete': ['https://www.googleapis.com/auth/drive.file']
 89 |       }
 90 |     }
 91 |   };
 92 | 
 93 |   async validate(endpoint: string): Promise<void> {
 94 |     // Parse endpoint into service and method
 95 |     const [service, ...methodParts] = endpoint.split('.');
 96 |     const methodName = methodParts.join('.');
 97 | 
 98 |     // Validate service exists
 99 |     if (!service || !this.serviceRegistry[service]) {
100 |       throw new GoogleApiError(
101 |         `Service "${service}" is not supported`,
102 |         'INVALID_SERVICE',
103 |         `Supported services are: ${Object.keys(this.serviceRegistry).join(', ')}`
104 |       );
105 |     }
106 | 
107 |     // Validate method exists
108 |     const serviceConfig = this.serviceRegistry[service];
109 |     if (!methodName || !serviceConfig.methods.includes(methodName)) {
110 |       throw new GoogleApiError(
111 |         `Method "${methodName}" is not supported for service "${service}"`,
112 |         'INVALID_METHOD',
113 |         `Available methods for ${service} are: ${serviceConfig.methods.join(', ')}`
114 |       );
115 |     }
116 |   }
117 | 
118 |   getRequiredScopes(endpoint: string): string[] {
119 |     const [service, ...methodParts] = endpoint.split('.');
120 |     const methodName = methodParts.join('.');
121 | 
122 |     const serviceConfig = this.serviceRegistry[service];
123 |     if (!serviceConfig || !serviceConfig.scopes[methodName]) {
124 |       return [];
125 |     }
126 | 
127 |     return serviceConfig.scopes[methodName];
128 |   }
129 | 
130 |   getSupportedServices(): string[] {
131 |     return Object.keys(this.serviceRegistry);
132 |   }
133 | 
134 |   getSupportedMethods(service: string): string[] {
135 |     return this.serviceRegistry[service]?.methods || [];
136 |   }
137 | }
138 | 
```
--------------------------------------------------------------------------------
/src/tools/drive-handlers.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
  2 | import { getAccountManager } from '../modules/accounts/index.js';
  3 | import { getDriveService } from '../modules/drive/index.js';
  4 | import { FileListOptions, FileSearchOptions, FileUploadOptions, PermissionOptions } from '../modules/drive/types.js';
  5 | import { McpToolResponse } from './types.js';
  6 | 
  7 | interface DriveFileListArgs {
  8 |   email: string;
  9 |   options?: FileListOptions;
 10 | }
 11 | 
 12 | interface DriveSearchArgs {
 13 |   email: string;
 14 |   options: FileSearchOptions;
 15 | }
 16 | 
 17 | interface DriveUploadArgs {
 18 |   email: string;
 19 |   options: FileUploadOptions;
 20 | }
 21 | 
 22 | interface DriveDownloadArgs {
 23 |   email: string;
 24 |   fileId: string;
 25 |   mimeType?: string;
 26 | }
 27 | 
 28 | interface DriveFolderArgs {
 29 |   email: string;
 30 |   name: string;
 31 |   parentId?: string;
 32 | }
 33 | 
 34 | interface DrivePermissionArgs {
 35 |   email: string;
 36 |   options: PermissionOptions;
 37 | }
 38 | 
 39 | interface DriveDeleteArgs {
 40 |   email: string;
 41 |   fileId: string;
 42 | }
 43 | 
 44 | export async function handleListDriveFiles(args: DriveFileListArgs): Promise<McpToolResponse> {
 45 |   const accountManager = getAccountManager();
 46 |   
 47 |   return await accountManager.withTokenRenewal(args.email, async () => {
 48 |     const driveService = await getDriveService();
 49 |     const result = await driveService.listFiles(args.email, args.options || {});
 50 |     return {
 51 |       content: [{
 52 |         type: 'text',
 53 |         text: JSON.stringify(result, null, 2)
 54 |       }]
 55 |     };
 56 |   });
 57 | }
 58 | 
 59 | export async function handleSearchDriveFiles(args: DriveSearchArgs): Promise<McpToolResponse> {
 60 |   const accountManager = getAccountManager();
 61 |   
 62 |   return await accountManager.withTokenRenewal(args.email, async () => {
 63 |     const driveService = await getDriveService();
 64 |     const result = await driveService.searchFiles(args.email, args.options);
 65 |     return {
 66 |       content: [{
 67 |         type: 'text',
 68 |         text: JSON.stringify(result, null, 2)
 69 |       }]
 70 |     };
 71 |   });
 72 | }
 73 | 
 74 | export async function handleUploadDriveFile(args: DriveUploadArgs): Promise<McpToolResponse> {
 75 |   if (!args.options.name) {
 76 |     throw new McpError(ErrorCode.InvalidParams, 'File name is required');
 77 |   }
 78 |   if (!args.options.content) {
 79 |     throw new McpError(ErrorCode.InvalidParams, 'File content is required');
 80 |   }
 81 | 
 82 |   const accountManager = getAccountManager();
 83 |   
 84 |   return await accountManager.withTokenRenewal(args.email, async () => {
 85 |     const driveService = await getDriveService();
 86 |     const result = await driveService.uploadFile(args.email, args.options);
 87 |     return {
 88 |       content: [{
 89 |         type: 'text',
 90 |         text: JSON.stringify(result, null, 2)
 91 |       }]
 92 |     };
 93 |   });
 94 | }
 95 | 
 96 | export async function handleDownloadDriveFile(args: DriveDownloadArgs): Promise<McpToolResponse> {
 97 |   if (!args.fileId) {
 98 |     throw new McpError(ErrorCode.InvalidParams, 'File ID is required');
 99 |   }
100 | 
101 |   const accountManager = getAccountManager();
102 |   
103 |   return await accountManager.withTokenRenewal(args.email, async () => {
104 |     const driveService = await getDriveService();
105 |     const result = await driveService.downloadFile(args.email, {
106 |       fileId: args.fileId,
107 |       mimeType: args.mimeType
108 |     });
109 |     return {
110 |       content: [{
111 |         type: 'text',
112 |         text: JSON.stringify(result, null, 2)
113 |       }]
114 |     };
115 |   });
116 | }
117 | 
118 | export async function handleCreateDriveFolder(args: DriveFolderArgs): Promise<McpToolResponse> {
119 |   if (!args.name) {
120 |     throw new McpError(ErrorCode.InvalidParams, 'Folder name is required');
121 |   }
122 | 
123 |   const accountManager = getAccountManager();
124 |   
125 |   return await accountManager.withTokenRenewal(args.email, async () => {
126 |     const driveService = await getDriveService();
127 |     const result = await driveService.createFolder(args.email, args.name, args.parentId);
128 |     return {
129 |       content: [{
130 |         type: 'text',
131 |         text: JSON.stringify(result, null, 2)
132 |       }]
133 |     };
134 |   });
135 | }
136 | 
137 | export async function handleUpdateDrivePermissions(args: DrivePermissionArgs): Promise<McpToolResponse> {
138 |   if (!args.options.fileId) {
139 |     throw new McpError(ErrorCode.InvalidParams, 'File ID is required');
140 |   }
141 |   if (!args.options.role) {
142 |     throw new McpError(ErrorCode.InvalidParams, 'Permission role is required');
143 |   }
144 |   if (!args.options.type) {
145 |     throw new McpError(ErrorCode.InvalidParams, 'Permission type is required');
146 |   }
147 | 
148 |   const accountManager = getAccountManager();
149 |   
150 |   return await accountManager.withTokenRenewal(args.email, async () => {
151 |     const driveService = await getDriveService();
152 |     const result = await driveService.updatePermissions(args.email, args.options);
153 |     return {
154 |       content: [{
155 |         type: 'text',
156 |         text: JSON.stringify(result, null, 2)
157 |       }]
158 |     };
159 |   });
160 | }
161 | 
162 | export async function handleDeleteDriveFile(args: DriveDeleteArgs): Promise<McpToolResponse> {
163 |   if (!args.fileId) {
164 |     throw new McpError(ErrorCode.InvalidParams, 'File ID is required');
165 |   }
166 | 
167 |   const accountManager = getAccountManager();
168 |   
169 |   return await accountManager.withTokenRenewal(args.email, async () => {
170 |     const driveService = await getDriveService();
171 |     const result = await driveService.deleteFile(args.email, args.fileId);
172 |     return {
173 |       content: [{
174 |         type: 'text',
175 |         text: JSON.stringify(result, null, 2)
176 |       }]
177 |     };
178 |   });
179 | }
180 | 
```
--------------------------------------------------------------------------------
/docs/ERRORS.md:
--------------------------------------------------------------------------------
```markdown
  1 | # Error Handling Documentation
  2 | 
  3 | ## Error Response Format
  4 | 
  5 | All errors follow a standardized format:
  6 | ```typescript
  7 | {
  8 |   status: "error",
  9 |   error: string,      // Human-readable error message
 10 |   resolution: string  // Suggested steps to resolve the error
 11 | }
 12 | ```
 13 | 
 14 | ## Error Categories
 15 | 
 16 | ### 1. Validation Errors
 17 | 
 18 | #### INVALID_SERVICE
 19 | - **Description**: The requested Google API service is not supported
 20 | - **Possible Causes**:
 21 |   - Service name is misspelled
 22 |   - Service is not implemented in the server
 23 | - **Resolution**: Check supported services in API documentation
 24 | 
 25 | #### INVALID_METHOD
 26 | - **Description**: The requested API method is not supported
 27 | - **Possible Causes**:
 28 |   - Method name is misspelled
 29 |   - Method is not implemented for the service
 30 | - **Resolution**: Verify method name in API documentation
 31 | 
 32 | #### INVALID_ENDPOINT
 33 | - **Description**: Malformed API endpoint format
 34 | - **Possible Causes**:
 35 |   - Incorrect endpoint format
 36 |   - Missing service or method parts
 37 | - **Resolution**: Use format "service.method" (e.g., "drive.files.list")
 38 | 
 39 | #### MISSING_REQUIRED_PARAMS
 40 | - **Description**: Required parameters are missing from the request
 41 | - **Possible Causes**:
 42 |   - Required parameters not provided
 43 |   - Parameters misspelled
 44 | - **Resolution**: Check required parameters in API documentation
 45 | 
 46 | #### INVALID_PARAM_TYPE
 47 | - **Description**: Parameter value type doesn't match expected type
 48 | - **Possible Causes**:
 49 |   - Wrong data type provided
 50 |   - Array provided instead of string or vice versa
 51 | - **Resolution**: Verify parameter types in API documentation
 52 | 
 53 | ### 2. Authentication Errors
 54 | 
 55 | #### TOKEN_EXPIRED
 56 | - **Description**: OAuth token has expired
 57 | - **Possible Causes**:
 58 |   - Token age exceeds expiration time
 59 |   - Token invalidated by user
 60 | - **Resolution**: Server will automatically refresh token, retry request
 61 | 
 62 | #### TOKEN_INVALID
 63 | - **Description**: OAuth token is invalid
 64 | - **Possible Causes**:
 65 |   - Token revoked
 66 |   - Token malformed
 67 | - **Resolution**: Please authenticate the account, which will grant all necessary permissions
 68 | 
 69 | #### INSUFFICIENT_SCOPE
 70 | - **Description**: Account needs authentication
 71 | - **Possible Causes**:
 72 |   - Account not authenticated
 73 |   - New permissions needed
 74 | - **Resolution**: Please authenticate the account, which will grant all necessary permissions
 75 | 
 76 | ### 3. API Errors
 77 | 
 78 | #### API_ERROR_400
 79 | - **Description**: Bad Request
 80 | - **Possible Causes**:
 81 |   - Invalid request parameters
 82 |   - Malformed request body
 83 | - **Resolution**: Check request format and parameters
 84 | 
 85 | #### API_ERROR_401
 86 | - **Description**: Unauthorized
 87 | - **Possible Causes**:
 88 |   - Invalid credentials
 89 |   - Token expired
 90 | - **Resolution**: Check authentication status
 91 | 
 92 | #### API_ERROR_403
 93 | - **Description**: Forbidden
 94 | - **Possible Causes**:
 95 |   - Insufficient permissions
 96 |   - Account restrictions
 97 | - **Resolution**: Please authenticate the account, which will grant all necessary permissions
 98 | 
 99 | #### API_ERROR_404
100 | - **Description**: Resource Not Found
101 | - **Possible Causes**:
102 |   - Invalid resource ID
103 |   - Resource deleted
104 | - **Resolution**: Verify resource exists
105 | 
106 | #### API_ERROR_429
107 | - **Description**: Rate Limit Exceeded
108 | - **Possible Causes**:
109 |   - Too many requests
110 |   - Quota exceeded
111 | - **Resolution**: Implement exponential backoff
112 | 
113 | #### API_ERROR_500
114 | - **Description**: Internal Server Error
115 | - **Possible Causes**:
116 |   - Google API error
117 |   - Server-side issue
118 | - **Resolution**: Retry request later
119 | 
120 | #### API_ERROR_503
121 | - **Description**: Service Unavailable
122 | - **Possible Causes**:
123 |   - Google API maintenance
124 |   - Temporary outage
125 | - **Resolution**: Retry request later
126 | 
127 | ### 4. System Errors
128 | 
129 | #### SERVICE_INIT_FAILED
130 | - **Description**: Failed to initialize Google service
131 | - **Possible Causes**:
132 |   - Configuration issues
133 |   - Network problems
134 | - **Resolution**: Check server configuration
135 | 
136 | #### NETWORK_ERROR
137 | - **Description**: Network communication failed
138 | - **Possible Causes**:
139 |   - Connection issues
140 |   - Timeout
141 | - **Resolution**: Check network connection and retry
142 | 
143 | ## Error Handling Best Practices
144 | 
145 | 1. **Always Check Status**
146 |    - Verify response status before processing
147 |    - Handle both success and error cases
148 | 
149 | 2. **Implement Retry Logic**
150 |    - Retry on transient errors (429, 500, 503)
151 |    - Use exponential backoff
152 |    - Set maximum retry attempts
153 | 
154 | 3. **Handle Authentication Flows**
155 |    - Watch for token expiration
156 |    - Handle refresh token flow
157 |    - Re-authenticate when needed
158 | 
159 | 4. **Log Errors Appropriately**
160 |    - Include error codes and messages
161 |    - Log stack traces for debugging
162 |    - Don't log sensitive information
163 | 
164 | 5. **User Communication**
165 |    - Provide clear, user-friendly error messages
166 |    - Include simple resolution steps
167 |    - Avoid technical jargon in user-facing messages
168 |    - Offer support contact if needed
169 | 
170 | ## Example Error Handling
171 | 
172 | ```typescript
173 | try {
174 |   const response = await makeRequest();
175 |   if (response.status === "error") {
176 |     // Handle error based on type
177 |     switch (response.error) {
178 |       case "TOKEN_EXPIRED":
179 |         // Handle token refresh
180 |         break;
181 |       case "INVALID_PARAM_TYPE":
182 |         // Fix parameters and retry
183 |         break;
184 |       // ... handle other cases
185 |     }
186 |   }
187 | } catch (error) {
188 |   // Handle unexpected errors
189 |   console.error("Request failed:", error);
190 | }
191 | 
```
--------------------------------------------------------------------------------
/src/__tests__/modules/attachments/index.test.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { AttachmentIndexService } from '../../../modules/attachments/index-service.js';
  2 | import { AttachmentCleanupService } from '../../../modules/attachments/cleanup-service.js';
  3 | import { AttachmentResponseTransformer } from '../../../modules/attachments/response-transformer.js';
  4 | 
  5 | describe('Attachment System', () => {
  6 |   let indexService: AttachmentIndexService;
  7 |   let cleanupService: AttachmentCleanupService;
  8 |   let responseTransformer: AttachmentResponseTransformer;
  9 | 
 10 |   beforeEach(() => {
 11 |     // Reset singleton instance for test isolation
 12 |     // @ts-expect-error - Accessing private static for testing
 13 |     AttachmentIndexService.instance = undefined;
 14 |     indexService = AttachmentIndexService.getInstance();
 15 |     cleanupService = new AttachmentCleanupService(indexService);
 16 |     responseTransformer = new AttachmentResponseTransformer(indexService);
 17 |   });
 18 | 
 19 |   afterEach(() => {
 20 |     cleanupService.stop();
 21 |     jest.clearAllTimers();
 22 |   });
 23 | 
 24 |   describe('AttachmentIndexService', () => {
 25 |     it('should store and retrieve attachment metadata', () => {
 26 |       const messageId = 'msg123';
 27 |       const attachment = {
 28 |         id: 'att123',
 29 |         name: 'test.pdf',
 30 |         mimeType: 'application/pdf',
 31 |         size: 1024
 32 |       };
 33 | 
 34 |       indexService.addAttachment(messageId, attachment);
 35 |       const metadata = indexService.getMetadata(messageId, 'test.pdf');
 36 | 
 37 |       expect(metadata).toBeDefined();
 38 |       expect(metadata?.originalId).toBe('att123');
 39 |       expect(metadata?.filename).toBe('test.pdf');
 40 |       expect(metadata?.mimeType).toBe('application/pdf');
 41 |       expect(metadata?.size).toBe(1024);
 42 |     });
 43 | 
 44 |     it('should handle size limits', () => {
 45 |       // Add max entries + 1
 46 |       for (let i = 0; i < 257; i++) {
 47 |         indexService.addAttachment(`msg${i}`, {
 48 |           id: `att${i}`,
 49 |           name: 'test.pdf',
 50 |           mimeType: 'application/pdf',
 51 |           size: 1024
 52 |         });
 53 |       }
 54 | 
 55 |       // Size should be maintained at or below max
 56 |       expect(indexService.size).toBeLessThanOrEqual(256);
 57 |     });
 58 | 
 59 |     it('should handle expiry', () => {
 60 |       const messageId = 'msg123';
 61 |       const attachment = {
 62 |         id: 'att123',
 63 |         name: 'test.pdf',
 64 |         mimeType: 'application/pdf',
 65 |         size: 1024
 66 |       };
 67 | 
 68 |       // Add attachment
 69 |       indexService.addAttachment(messageId, attachment);
 70 |       
 71 |       // Mock time passing
 72 |       jest.useFakeTimers();
 73 |       jest.advanceTimersByTime(3600000 + 1000); // 1 hour + 1 second
 74 | 
 75 |       // Attempt to retrieve expired attachment
 76 |       const metadata = indexService.getMetadata(messageId, 'test.pdf');
 77 |       expect(metadata).toBeUndefined();
 78 | 
 79 |       jest.useRealTimers();
 80 |     });
 81 |   });
 82 | 
 83 |   describe('AttachmentCleanupService', () => {
 84 |     it('should start and stop cleanup service', () => {
 85 |       cleanupService.start();
 86 |       expect(cleanupService.getCurrentInterval()).toBe(300000); // Base interval
 87 |       cleanupService.stop();
 88 |       expect(cleanupService.getCurrentInterval()).toBe(300000);
 89 |     });
 90 |   });
 91 | 
 92 |   describe('AttachmentResponseTransformer', () => {
 93 |     it('should transform attachments to simplified format', () => {
 94 |       const messageId = 'msg123';
 95 |       const fullResponse = {
 96 |         id: messageId,
 97 |         attachments: [{
 98 |           id: 'att123',
 99 |           name: 'test.pdf',
100 |           mimeType: 'application/pdf',
101 |           size: 1024
102 |         }]
103 |       };
104 | 
105 |       // Store attachment metadata
106 |       indexService.addAttachment(messageId, fullResponse.attachments[0]);
107 | 
108 |       // Transform response
109 |       const simplified = responseTransformer.transformResponse(fullResponse);
110 | 
111 |       expect(simplified).toEqual({
112 |         id: messageId,
113 |         attachments: [{
114 |           name: 'test.pdf'
115 |         }]
116 |       });
117 |     });
118 | 
119 |     it('should handle responses without attachments', () => {
120 |       const response = {
121 |         id: 'msg123',
122 |         subject: 'Test'
123 |       };
124 | 
125 |       const simplified = responseTransformer.transformResponse(response);
126 |       expect(simplified).toEqual(response);
127 |     });
128 |   });
129 | 
130 |   describe('Integration', () => {
131 |     it('should maintain attachment metadata through transform cycle', () => {
132 |       const messageId = 'msg123';
133 |       const originalAttachment = {
134 |         id: 'att123',
135 |         name: 'test.pdf',
136 |         mimeType: 'application/pdf',
137 |         size: 1024
138 |       };
139 | 
140 |       // Add attachment and transform response
141 |       indexService.addAttachment(messageId, originalAttachment);
142 |       const transformed = responseTransformer.transformResponse({
143 |         id: messageId,
144 |         attachments: [originalAttachment]
145 |       });
146 | 
147 |       // Verify simplified format
148 |       expect(transformed.attachments?.[0]).toEqual({
149 |         name: 'test.pdf'
150 |       });
151 | 
152 |       // Verify original metadata is preserved
153 |       const metadata = indexService.getMetadata(messageId, 'test.pdf');
154 |       expect(metadata).toEqual({
155 |         messageId,
156 |         filename: 'test.pdf',
157 |         originalId: 'att123',
158 |         mimeType: 'application/pdf',
159 |         size: 1024,
160 |         timestamp: expect.any(Number)
161 |       });
162 |     });
163 | 
164 |     it('should notify activity without error', () => {
165 |       cleanupService.start();
166 |       cleanupService.notifyActivity(); // Should not throw
167 |       cleanupService.stop();
168 |     });
169 |   });
170 | });
171 | 
```
--------------------------------------------------------------------------------
/src/api/validators/parameter.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { GoogleApiRequestParams, GoogleApiError } from '../../types.js';
  2 | 
  3 | interface ParameterConfig {
  4 |   required: string[];
  5 |   types: Record<string, string>;
  6 | }
  7 | 
  8 | export class ParameterValidator {
  9 |   // Registry of endpoint-specific parameter configurations
 10 |   private readonly parameterRegistry: Record<string, ParameterConfig> = {
 11 |     'gmail.users.messages.attachments.get': {
 12 |       required: ['userId', 'messageId', 'filename'],
 13 |       types: {
 14 |         userId: 'string',
 15 |         messageId: 'string',
 16 |         filename: 'string'
 17 |       }
 18 |     },
 19 |     'gmail.users.messages.attachments.upload': {
 20 |       required: ['userId', 'messageId', 'filename', 'content'],
 21 |       types: {
 22 |         userId: 'string',
 23 |         messageId: 'string',
 24 |         filename: 'string',
 25 |         content: 'string'
 26 |       }
 27 |     },
 28 |     'gmail.users.messages.attachments.delete': {
 29 |       required: ['userId', 'messageId', 'filename'],
 30 |       types: {
 31 |         userId: 'string',
 32 |         messageId: 'string',
 33 |         filename: 'string'
 34 |       }
 35 |     },
 36 |     'calendar.events.attachments.get': {
 37 |       required: ['calendarId', 'eventId', 'filename'],
 38 |       types: {
 39 |         calendarId: 'string',
 40 |         eventId: 'string',
 41 |         filename: 'string'
 42 |       }
 43 |     },
 44 |     'calendar.events.attachments.upload': {
 45 |       required: ['calendarId', 'eventId', 'filename', 'content'],
 46 |       types: {
 47 |         calendarId: 'string',
 48 |         eventId: 'string',
 49 |         filename: 'string',
 50 |         content: 'string'
 51 |       }
 52 |     },
 53 |     'calendar.events.attachments.delete': {
 54 |       required: ['calendarId', 'eventId', 'filename'],
 55 |       types: {
 56 |         calendarId: 'string',
 57 |         eventId: 'string',
 58 |         filename: 'string'
 59 |       }
 60 |     },
 61 |     'gmail.users.messages.list': {
 62 |       required: ['userId'],
 63 |       types: {
 64 |         userId: 'string',
 65 |         maxResults: 'number',
 66 |         pageToken: 'string',
 67 |         q: 'string',
 68 |         labelIds: 'array'
 69 |       }
 70 |     },
 71 |     'gmail.users.messages.get': {
 72 |       required: ['userId', 'id'],
 73 |       types: {
 74 |         userId: 'string',
 75 |         id: 'string',
 76 |         format: 'string'
 77 |       }
 78 |     },
 79 |     'gmail.users.messages.send': {
 80 |       required: ['userId', 'message'],
 81 |       types: {
 82 |         userId: 'string',
 83 |         message: 'object'
 84 |       }
 85 |     },
 86 |     'drive.files.list': {
 87 |       required: [],
 88 |       types: {
 89 |         pageSize: 'number',
 90 |         pageToken: 'string',
 91 |         q: 'string',
 92 |         spaces: 'string',
 93 |         fields: 'string'
 94 |       }
 95 |     },
 96 |     'drive.files.get': {
 97 |       required: ['fileId'],
 98 |       types: {
 99 |         fileId: 'string',
100 |         fields: 'string',
101 |         acknowledgeAbuse: 'boolean'
102 |       }
103 |     }
104 |   };
105 | 
106 |   async validate(params: GoogleApiRequestParams): Promise<void> {
107 |     const { api_endpoint, params: methodParams = {} } = params;
108 | 
109 |     // Get parameter configuration for this endpoint
110 |     const config = this.parameterRegistry[api_endpoint];
111 |     if (!config) {
112 |       // If no specific config exists, only validate the base required params
113 |       this.validateBaseParams(params);
114 |       return;
115 |     }
116 | 
117 |     // Validate required parameters
118 |     this.validateRequiredParams(api_endpoint, methodParams, config.required);
119 | 
120 |     // Validate parameter types
121 |     this.validateParamTypes(api_endpoint, methodParams, config.types);
122 |   }
123 | 
124 |   private validateBaseParams(params: GoogleApiRequestParams): void {
125 |     const requiredBaseParams = ['email', 'api_endpoint', 'method', 'required_scopes'];
126 |     const missingParams = requiredBaseParams.filter(param => !(param in params));
127 | 
128 |     if (missingParams.length > 0) {
129 |       throw new GoogleApiError(
130 |         'Missing required parameters',
131 |         'MISSING_REQUIRED_PARAMS',
132 |         `The following parameters are required: ${missingParams.join(', ')}`
133 |       );
134 |     }
135 |   }
136 | 
137 |   private validateRequiredParams(
138 |     endpoint: string,
139 |     params: Record<string, any>,
140 |     required: string[]
141 |   ): void {
142 |     const missingParams = required.filter(param => !(param in params));
143 | 
144 |     if (missingParams.length > 0) {
145 |       throw new GoogleApiError(
146 |         'Missing required parameters',
147 |         'MISSING_REQUIRED_PARAMS',
148 |         `The following parameters are required for ${endpoint}: ${missingParams.join(', ')}`
149 |       );
150 |     }
151 |   }
152 | 
153 |   private validateParamTypes(
154 |     endpoint: string,
155 |     params: Record<string, any>,
156 |     types: Record<string, string>
157 |   ): void {
158 |     for (const [param, value] of Object.entries(params)) {
159 |       const expectedType = types[param];
160 |       if (!expectedType) {
161 |         // Skip validation for parameters not in the type registry
162 |         continue;
163 |       }
164 | 
165 |       const actualType = this.getType(value);
166 |       if (actualType !== expectedType) {
167 |         throw new GoogleApiError(
168 |           'Invalid parameter type',
169 |           'INVALID_PARAM_TYPE',
170 |           `Parameter "${param}" for ${endpoint} must be of type ${expectedType}, got ${actualType}`
171 |         );
172 |       }
173 |     }
174 |   }
175 | 
176 |   private getType(value: any): string {
177 |     if (Array.isArray(value)) return 'array';
178 |     if (value === null) return 'null';
179 |     if (typeof value === 'object') return 'object';
180 |     return typeof value;
181 |   }
182 | 
183 |   getRequiredParams(endpoint: string): string[] {
184 |     return this.parameterRegistry[endpoint]?.required || [];
185 |   }
186 | 
187 |   getParamTypes(endpoint: string): Record<string, string> {
188 |     return this.parameterRegistry[endpoint]?.types || {};
189 |   }
190 | }
191 | 
```
--------------------------------------------------------------------------------
/src/oauth/client.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import fs from 'fs/promises';
  2 | import path from 'path';
  3 | import { google } from 'googleapis';
  4 | import { OAuth2Client } from 'google-auth-library';
  5 | import { OAuthConfig, TokenData, GoogleApiError } from '../types.js';
  6 | 
  7 | export class GoogleOAuthClient {
  8 |   private client?: OAuth2Client;
  9 |   private config?: OAuthConfig;
 10 |   private initializationPromise?: Promise<void>;
 11 | 
 12 |   async ensureInitialized(): Promise<void> {
 13 |     if (!this.initializationPromise) {
 14 |       this.initializationPromise = this.loadConfig().catch(error => {
 15 |         // Clear the promise so we can retry initialization
 16 |         this.initializationPromise = undefined;
 17 |         throw error;
 18 |       });
 19 |     }
 20 |     await this.initializationPromise;
 21 |   }
 22 | 
 23 |   private async loadConfig(): Promise<void> {
 24 |     // First try environment variables
 25 |     const clientId = process.env.GOOGLE_CLIENT_ID;
 26 |     const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
 27 | 
 28 |     if (clientId && clientSecret) {
 29 |       this.config = {
 30 |         client_id: clientId,
 31 |         client_secret: clientSecret,
 32 |         auth_uri: 'https://accounts.google.com/o/oauth2/v2/auth',
 33 |         token_uri: 'https://oauth2.googleapis.com/token'
 34 |       };
 35 |     } else {
 36 |       // Fall back to config file if environment variables are not set
 37 |       try {
 38 |         const configPath = process.env.GAUTH_FILE || path.resolve('config', 'gauth.json');
 39 |         const data = await fs.readFile(configPath, 'utf-8');
 40 |         this.config = JSON.parse(data) as OAuthConfig;
 41 |       } catch (error) {
 42 |         if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
 43 |           throw new GoogleApiError(
 44 |             'OAuth credentials not found',
 45 |             'CONFIG_NOT_FOUND',
 46 |             'Please provide GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables or ensure config/gauth.json exists'
 47 |           );
 48 |         }
 49 |         throw new GoogleApiError(
 50 |           'Failed to load OAuth configuration',
 51 |           'OAUTH_CONFIG_ERROR',
 52 |           'Please check your environment variables or ensure gauth.json is valid'
 53 |         );
 54 |       }
 55 |     }
 56 | 
 57 |     if (!this.config) {
 58 |       throw new GoogleApiError(
 59 |         'OAuth configuration not available',
 60 |         'CONFIG_NOT_FOUND',
 61 |         'Please provide OAuth credentials through environment variables or config file'
 62 |       );
 63 |     }
 64 | 
 65 |     this.client = new google.auth.OAuth2(
 66 |       this.config.client_id,
 67 |       this.config.client_secret,
 68 |       'urn:ietf:wg:oauth:2.0:oob'  // Use device code flow
 69 |     );
 70 |   }
 71 | 
 72 |   async generateAuthUrl(scopes: string[]): Promise<string> {
 73 |     await this.ensureInitialized();
 74 |     if (!this.config || !this.client) {
 75 |       throw new GoogleApiError(
 76 |         'OAuth client not initialized',
 77 |         'CLIENT_NOT_INITIALIZED',
 78 |         'Please ensure the OAuth configuration is loaded'
 79 |       );
 80 |     }
 81 | 
 82 |     return this.client.generateAuthUrl({
 83 |       access_type: 'offline',
 84 |       scope: scopes,
 85 |       prompt: 'consent'  // Force consent screen to ensure we get refresh token
 86 |     });
 87 |   }
 88 | 
 89 |   async getTokenFromCode(code: string): Promise<TokenData> {
 90 |     await this.ensureInitialized();
 91 |     if (!this.client) {
 92 |       throw new GoogleApiError(
 93 |         'OAuth client not initialized',
 94 |         'CLIENT_NOT_INITIALIZED',
 95 |         'Please ensure the OAuth configuration is loaded'
 96 |       );
 97 |     }
 98 | 
 99 |     try {
100 |       const { tokens } = await this.client.getToken(code);
101 |       
102 |       if (!tokens.refresh_token) {
103 |         throw new GoogleApiError(
104 |           'No refresh token received',
105 |           'NO_REFRESH_TOKEN',
106 |           'Please ensure you have included offline access in your scopes'
107 |         );
108 |       }
109 | 
110 |       return {
111 |         access_token: tokens.access_token!,
112 |         refresh_token: tokens.refresh_token,
113 |         scope: tokens.scope!,
114 |         token_type: tokens.token_type!,
115 |         expiry_date: tokens.expiry_date!,
116 |         last_refresh: Date.now()
117 |       };
118 |     } catch (error) {
119 |       if (error instanceof GoogleApiError) {
120 |         throw error;
121 |       }
122 |       throw new GoogleApiError(
123 |         'Failed to get token from code',
124 |         'TOKEN_EXCHANGE_ERROR',
125 |         'The authorization code may be invalid or expired'
126 |       );
127 |     }
128 |   }
129 | 
130 |   async refreshToken(refreshToken: string): Promise<TokenData> {
131 |     await this.ensureInitialized();
132 |     if (!this.client) {
133 |       throw new GoogleApiError(
134 |         'OAuth client not initialized',
135 |         'CLIENT_NOT_INITIALIZED',
136 |         'Please ensure the OAuth configuration is loaded'
137 |       );
138 |     }
139 | 
140 |     try {
141 |       this.client.setCredentials({
142 |         refresh_token: refreshToken
143 |       });
144 | 
145 |       const { credentials } = await this.client.refreshAccessToken();
146 |       
147 |       return {
148 |         access_token: credentials.access_token!,
149 |         refresh_token: refreshToken, // Keep existing refresh token
150 |         scope: credentials.scope!,
151 |         token_type: credentials.token_type!,
152 |         expiry_date: credentials.expiry_date!,
153 |         last_refresh: Date.now()
154 |       };
155 |     } catch (error) {
156 |       throw new GoogleApiError(
157 |         'Failed to refresh token',
158 |         'TOKEN_REFRESH_ERROR',
159 |         'The refresh token may be invalid or revoked'
160 |       );
161 |     }
162 |   }
163 | 
164 |   async validateToken(token: TokenData): Promise<boolean> {
165 |     await this.ensureInitialized();
166 |     if (!this.client) {
167 |       throw new GoogleApiError(
168 |         'OAuth client not initialized',
169 |         'CLIENT_NOT_INITIALIZED',
170 |         'Please ensure the OAuth configuration is loaded'
171 |       );
172 |     }
173 | 
174 |     try {
175 |       this.client.setCredentials({
176 |         access_token: token.access_token,
177 |         refresh_token: token.refresh_token
178 |       });
179 | 
180 |       await this.client.getTokenInfo(token.access_token);
181 |       return true;
182 |     } catch (error) {
183 |       return false;
184 |     }
185 |   }
186 | 
187 |   async getAuthClient(): Promise<OAuth2Client> {
188 |     await this.ensureInitialized();
189 |     if (!this.client) {
190 |       throw new GoogleApiError(
191 |         'OAuth client not initialized',
192 |         'CLIENT_NOT_INITIALIZED',
193 |         'Please ensure the OAuth configuration is loaded'
194 |       );
195 |     }
196 |     return this.client;
197 |   }
198 | }
199 | 
```
--------------------------------------------------------------------------------
/src/tools/account-handlers.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { getAccountManager } from '../modules/accounts/index.js';
  2 | import { McpToolResponse, BaseToolArguments } from './types.js';
  3 | 
  4 | /**
  5 |  * Lists all configured Google Workspace accounts and their authentication status
  6 |  * @returns List of accounts with their configuration and auth status
  7 |  * @throws {McpError} If account manager fails to retrieve accounts
  8 |  */
  9 | export async function handleListWorkspaceAccounts(): Promise<McpToolResponse> {
 10 |   const accounts = await getAccountManager().listAccounts();
 11 |   
 12 |   // Filter out sensitive token data before returning to AI
 13 |   const sanitizedAccounts = accounts.map(account => ({
 14 |     ...account,
 15 |     auth_status: account.auth_status ? {
 16 |       valid: account.auth_status.valid,
 17 |       status: account.auth_status.status,
 18 |       reason: account.auth_status.reason,
 19 |       authUrl: account.auth_status.authUrl
 20 |     } : undefined
 21 |   }));
 22 | 
 23 |   return {
 24 |     content: [{
 25 |       type: 'text',
 26 |       text: JSON.stringify(sanitizedAccounts, null, 2)
 27 |     }]
 28 |   };
 29 | }
 30 | 
 31 | export interface AuthenticateAccountArgs extends BaseToolArguments {
 32 |   category?: string;
 33 |   description?: string;
 34 |   auth_code?: string;
 35 |   auto_complete?: boolean;
 36 | }
 37 | 
 38 | /**
 39 |  * Authenticates a Google Workspace account through OAuth2
 40 |  * @param args.email - Email address to authenticate
 41 |  * @param args.category - Optional account category (e.g., 'work', 'personal')
 42 |  * @param args.description - Optional account description
 43 |  * @param args.auth_code - OAuth2 authorization code (optional for manual flow)
 44 |  * @param args.auto_complete - Whether to automatically complete auth (default: true)
 45 |  * @returns Auth URL and instructions for completing authentication
 46 |  * @throws {McpError} If validation fails or OAuth flow errors
 47 |  */
 48 | export async function handleAuthenticateWorkspaceAccount(args: AuthenticateAccountArgs): Promise<McpToolResponse> {
 49 |   const accountManager = getAccountManager();
 50 | 
 51 |   // Validate/create account
 52 |   await accountManager.validateAccount(args.email, args.category, args.description);
 53 | 
 54 |   // If auth code is provided (manual fallback), complete the OAuth flow
 55 |   if (args.auth_code) {
 56 |     const tokenData = await accountManager.getTokenFromCode(args.auth_code);
 57 |     await accountManager.saveToken(args.email, tokenData);
 58 |     return {
 59 |       content: [{
 60 |         type: 'text',
 61 |         text: JSON.stringify({
 62 |           status: 'success',
 63 |           message: 'Authentication successful! Token saved. Please retry your request.'
 64 |         }, null, 2)
 65 |       }]
 66 |     };
 67 |   }
 68 | 
 69 |   // Generate OAuth URL and track which account is being authenticated
 70 |   const authUrl = await accountManager.startAuthentication(args.email);
 71 |   
 72 |   // Check if we should use automatic completion (default: true)
 73 |   const useAutoComplete = args.auto_complete !== false;
 74 |   
 75 |   return {
 76 |     content: [{
 77 |       type: 'text',
 78 |       text: JSON.stringify({
 79 |         status: 'auth_required',
 80 |         auth_url: authUrl,
 81 |         message: 'Please complete Google OAuth authentication:',
 82 |         instructions: useAutoComplete ? [
 83 |           '1. Click the authorization URL to open Google sign-in in your browser',
 84 |           '2. Sign in with your Google account and allow the requested permissions',
 85 |           '3. Authentication will complete automatically - you can start using the account immediately'
 86 |         ].join('\n') : [
 87 |           '1. Click the authorization URL below to open Google sign-in in your browser',
 88 |           '2. Sign in with your Google account and allow the requested permissions',
 89 |           '3. After authorization, you will see a success page with your authorization code',
 90 |           '4. Copy the authorization code from the success page',
 91 |           '5. Call this tool again with the auth_code parameter: authenticate_workspace_account with auth_code="your_code_here"'
 92 |         ].join('\n'),
 93 |         note: useAutoComplete 
 94 |           ? 'The callback server will automatically complete authentication in the background.'
 95 |           : 'The callback server is running on localhost:8080 and will display your authorization code for easy copying.',
 96 |         auto_complete_enabled: useAutoComplete
 97 |       }, null, 2)
 98 |     }]
 99 |   };
100 | }
101 | 
102 | /**
103 |  * Completes OAuth authentication automatically by waiting for callback
104 |  * @param args.email - Email address to authenticate
105 |  * @returns Success message when authentication completes
106 |  * @throws {McpError} If authentication times out or fails
107 |  */
108 | export async function handleCompleteWorkspaceAuth(args: BaseToolArguments): Promise<McpToolResponse> {
109 |   const accountManager = getAccountManager();
110 |   
111 |   try {
112 |     // Wait for the authorization code from the callback server
113 |     const code = await accountManager.waitForAuthorizationCode();
114 |     
115 |     // Exchange code for tokens
116 |     const tokenData = await accountManager.getTokenFromCode(code);
117 |     await accountManager.saveToken(args.email, tokenData);
118 |     
119 |     return {
120 |       content: [{
121 |         type: 'text',
122 |         text: JSON.stringify({
123 |           status: 'success',
124 |           message: 'Authentication completed automatically! Your account is now ready to use.'
125 |         }, null, 2)
126 |       }]
127 |     };
128 |   } catch (error) {
129 |     return {
130 |       content: [{
131 |         type: 'text',
132 |         text: JSON.stringify({
133 |           status: 'error',
134 |           message: 'Authentication timeout or error. Please use the manual authentication flow.',
135 |           error: error instanceof Error ? error.message : 'Unknown error'
136 |         }, null, 2)
137 |       }]
138 |     };
139 |   }
140 | }
141 | 
142 | /**
143 |  * Removes a Google Workspace account and its associated authentication tokens
144 |  * @param args.email - Email address of the account to remove
145 |  * @returns Success message if account removed
146 |  * @throws {McpError} If account removal fails
147 |  */
148 | export async function handleRemoveWorkspaceAccount(args: BaseToolArguments): Promise<McpToolResponse> {
149 |   await getAccountManager().removeAccount(args.email);
150 |   
151 |   return {
152 |     content: [{
153 |       type: 'text',
154 |       text: JSON.stringify({
155 |         status: 'success',
156 |         message: `Successfully removed account ${args.email} and deleted associated tokens`
157 |       }, null, 2)
158 |     }]
159 |   };
160 | }
161 | 
```
--------------------------------------------------------------------------------
/src/modules/gmail/types.ts:
--------------------------------------------------------------------------------
```typescript
  1 | export interface BaseGmailAttachment {
  2 |   id: string;          // Gmail attachment ID
  3 |   name: string;        // Filename
  4 |   mimeType: string;    // MIME type
  5 |   size: number;        // Size in bytes
  6 | }
  7 | 
  8 | export interface IncomingGmailAttachment extends BaseGmailAttachment {
  9 |   content?: string;    // Base64 content when retrieved
 10 |   path?: string;       // Local filesystem path when downloaded
 11 | }
 12 | 
 13 | export interface OutgoingGmailAttachment extends BaseGmailAttachment {
 14 |   content: string;     // Base64 content required when sending
 15 | }
 16 | 
 17 | export type GmailAttachment = IncomingGmailAttachment | OutgoingGmailAttachment;
 18 | 
 19 | export interface Label {
 20 |   id: string;
 21 |   name: string;
 22 |   messageListVisibility?: 'show' | 'hide';
 23 |   labelListVisibility?: 'labelShow' | 'labelHide' | 'labelShowIfUnread';
 24 |   type?: 'system' | 'user';
 25 |   color?: {
 26 |     textColor: string;
 27 |     backgroundColor: string;
 28 |   };
 29 | }
 30 | 
 31 | export interface DraftResponse {
 32 |   id: string;
 33 |   message: {
 34 |     id: string;
 35 |     threadId: string;
 36 |     labelIds: string[];
 37 |   };
 38 |   updated: string;
 39 | }
 40 | 
 41 | export interface GetDraftsResponse {
 42 |   drafts: DraftResponse[];
 43 |   nextPageToken?: string;
 44 |   resultSizeEstimate?: number;
 45 | }
 46 | 
 47 | export interface DraftEmailParams {
 48 |   to: string[];
 49 |   subject: string;
 50 |   body: string;
 51 |   cc?: string[];
 52 |   bcc?: string[];
 53 |   attachments?: {
 54 |     driveFileId?: string;
 55 |     content?: string;
 56 |     name: string;
 57 |     mimeType: string;
 58 |     size?: number;
 59 |   }[];
 60 | }
 61 | 
 62 | export interface GetDraftsParams {
 63 |   email: string;
 64 |   maxResults?: number;
 65 |   pageToken?: string;
 66 | }
 67 | 
 68 | export interface SendDraftParams {
 69 |   email: string;
 70 |   draftId: string;
 71 | }
 72 | 
 73 | export interface GetLabelsResponse {
 74 |   labels: Label[];
 75 |   nextPageToken?: string;
 76 | }
 77 | 
 78 | export interface LabelFilter {
 79 |   id: string;
 80 |   labelId: string;
 81 |   criteria: LabelFilterCriteria;
 82 |   actions: LabelFilterActions;
 83 | }
 84 | 
 85 | export interface GetLabelFiltersResponse {
 86 |   filters: LabelFilter[];
 87 | }
 88 | 
 89 | export interface CreateLabelParams {
 90 |   email: string;
 91 |   name: string;
 92 |   messageListVisibility?: 'show' | 'hide';
 93 |   labelListVisibility?: 'labelShow' | 'labelHide' | 'labelShowIfUnread';
 94 |   color?: {
 95 |     textColor: string;
 96 |     backgroundColor: string;
 97 |   };
 98 | }
 99 | 
100 | export interface UpdateLabelParams {
101 |   email: string;
102 |   labelId: string;
103 |   name?: string;
104 |   messageListVisibility?: 'show' | 'hide';
105 |   labelListVisibility?: 'labelShow' | 'labelHide' | 'labelShowIfUnread';
106 |   color?: {
107 |     textColor: string;
108 |     backgroundColor: string;
109 |   };
110 | }
111 | 
112 | export interface DeleteLabelParams {
113 |   email: string;
114 |   labelId: string;
115 | }
116 | 
117 | export interface GetLabelsParams {
118 |   email: string;
119 | }
120 | 
121 | export interface ModifyMessageLabelsParams {
122 |   email: string;
123 |   messageId: string;
124 |   addLabelIds: string[];
125 |   removeLabelIds: string[];
126 | }
127 | 
128 | export interface LabelFilterCriteria {
129 |   from?: string[];
130 |   to?: string[];
131 |   subject?: string;
132 |   hasWords?: string[];
133 |   doesNotHaveWords?: string[];
134 |   hasAttachment?: boolean;
135 |   size?: {
136 |     operator: 'larger' | 'smaller';
137 |     size: number;
138 |   };
139 | }
140 | 
141 | export interface LabelFilterActions {
142 |   addLabel?: boolean;
143 |   markImportant?: boolean;
144 |   markRead?: boolean;
145 |   archive?: boolean;
146 | }
147 | 
148 | export interface CreateLabelFilterParams {
149 |   email: string;
150 |   labelId: string;
151 |   criteria: LabelFilterCriteria;
152 |   actions: LabelFilterActions;
153 | }
154 | 
155 | export interface GetLabelFiltersParams {
156 |   email: string;
157 |   labelId?: string;
158 | }
159 | 
160 | export interface UpdateLabelFilterParams {
161 |   email: string;
162 |   filterId: string;
163 |   labelId: string;
164 |   criteria: LabelFilterCriteria;
165 |   actions: LabelFilterActions;
166 | }
167 | 
168 | export interface DeleteLabelFilterParams {
169 |   email: string;
170 |   filterId: string;
171 | }
172 | 
173 | export interface GetGmailSettingsParams {
174 |   email: string;
175 | }
176 | 
177 | export interface GetGmailSettingsResponse {
178 |   profile: {
179 |     emailAddress: string;
180 |     messagesTotal: number;
181 |     threadsTotal: number;
182 |     historyId: string;
183 |   };
184 |   settings: {
185 |     language?: {
186 |       displayLanguage: string;
187 |     };
188 |     autoForwarding?: {
189 |       enabled: boolean;
190 |       emailAddress?: string;
191 |     };
192 |     imap?: {
193 |       enabled: boolean;
194 |       autoExpunge?: boolean;
195 |       expungeBehavior?: string;
196 |     };
197 |     pop?: {
198 |       enabled: boolean;
199 |       accessWindow?: string;
200 |     };
201 |     vacationResponder?: {
202 |       enabled: boolean;
203 |       startTime?: string;
204 |       endTime?: string;
205 |       responseSubject?: string;
206 |       message?: string;
207 |     };
208 |   };
209 | }
210 | 
211 | export interface GmailModuleConfig {
212 |   clientId: string;
213 |   clientSecret: string;
214 |   redirectUri: string;
215 |   scopes: string[];
216 | }
217 | 
218 | export interface SearchCriteria {
219 |   from?: string | string[];
220 |   to?: string | string[];
221 |   subject?: string;
222 |   content?: string;
223 |   after?: string;
224 |   before?: string;
225 |   hasAttachment?: boolean;
226 |   labels?: string[];
227 |   excludeLabels?: string[];
228 |   includeSpam?: boolean;
229 |   isUnread?: boolean;
230 | }
231 | 
232 | export interface EmailResponse {
233 |   id: string;
234 |   threadId: string;
235 |   labelIds?: string[];
236 |   snippet?: string;
237 |   subject: string;
238 |   from: string;
239 |   to: string;
240 |   date: string;
241 |   body: string;
242 |   headers?: { [key: string]: string };
243 |   isUnread: boolean;
244 |   hasAttachment: boolean;
245 |   attachments?: IncomingGmailAttachment[];
246 | }
247 | 
248 | export interface ThreadInfo {
249 |   messages: string[];
250 |   participants: string[];
251 |   subject: string;
252 |   lastUpdated: string;
253 | }
254 | 
255 | export interface GetEmailsParams {
256 |   email: string;
257 |   search?: SearchCriteria;
258 |   options?: {
259 |     maxResults?: number;
260 |     pageToken?: string;
261 |     format?: 'full' | 'metadata' | 'minimal';
262 |     includeHeaders?: boolean;
263 |     threadedView?: boolean;
264 |     sortOrder?: 'asc' | 'desc';
265 |   };
266 |   messageIds?: string[];
267 | }
268 | 
269 | export interface GetEmailsResponse {
270 |   emails: EmailResponse[];
271 |   nextPageToken?: string;
272 |   resultSummary: {
273 |     total: number;
274 |     returned: number;
275 |     hasMore: boolean;
276 |     searchCriteria: SearchCriteria;
277 |   };
278 |   threads?: { [threadId: string]: ThreadInfo };
279 | }
280 | 
281 | export interface SendEmailParams {
282 |   email: string;
283 |   to: string[];
284 |   subject: string;
285 |   body: string;
286 |   cc?: string[];
287 |   bcc?: string[];
288 |   attachments?: OutgoingGmailAttachment[];
289 | }
290 | 
291 | export interface SendEmailResponse {
292 |   messageId: string;
293 |   threadId: string;
294 |   labelIds?: string[];
295 |   attachments?: GmailAttachment[];
296 | }
297 | 
298 | export class GmailError extends Error implements GmailError {
299 |   code: string;
300 |   details?: string;
301 | 
302 |   constructor(message: string, code: string, details?: string) {
303 |     super(message);
304 |     this.name = 'GmailError';
305 |     this.code = code;
306 |     this.details = details;
307 |   }
308 | }
309 | 
```
--------------------------------------------------------------------------------
/src/__helpers__/testSetup.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { gmail_v1 } from 'googleapis';
  2 | import type { AccountManager } from '../modules/accounts/manager.js' with { "resolution-mode": "import" };
  3 | import type { OAuth2Client } from 'google-auth-library' with { "resolution-mode": "import" };
  4 | 
  5 | // Basic mock token
  6 | const mockToken = {
  7 |   access_token: 'mock-access-token',
  8 |   refresh_token: 'mock-refresh-token',
  9 |   expiry_date: Date.now() + 3600000,
 10 | };
 11 | 
 12 | // Simple account manager mock
 13 | export const mockAccountManager = (): jest.Mocked<Partial<AccountManager>> => {
 14 |   const mockAuthClient: jest.Mocked<Partial<OAuth2Client>> = {
 15 |     setCredentials: jest.fn(),
 16 |   };
 17 | 
 18 |   return {
 19 |     initialize: jest.fn().mockResolvedValue(undefined),
 20 |     getAuthClient: jest.fn().mockResolvedValue(mockAuthClient),
 21 |     validateToken: jest.fn().mockResolvedValue({ valid: true, token: mockToken }),
 22 |     withTokenRenewal: jest.fn().mockImplementation((email, operation) => operation()),
 23 |   };
 24 | };
 25 | 
 26 | // Type for Gmail client that makes context optional but keeps users required
 27 | type MockGmailClient = Omit<gmail_v1.Gmail, 'context'> & { context?: gmail_v1.Gmail['context'] };
 28 | 
 29 | // Simple Gmail client mock with proper typing and success/failure cases
 30 | export const mockGmailClient: jest.Mocked<MockGmailClient> = {
 31 |   users: {
 32 |     messages: {
 33 |       list: jest.fn()
 34 |         .mockResolvedValueOnce({ // Success case with results
 35 |           data: {
 36 |             messages: [
 37 |               { id: 'msg-1', threadId: 'thread-1' },
 38 |               { id: 'msg-2', threadId: 'thread-1' }
 39 |             ],
 40 |             resultSizeEstimate: 2
 41 |           }
 42 |         })
 43 |         .mockResolvedValueOnce({ // Empty results case
 44 |           data: { 
 45 |             messages: [],
 46 |             resultSizeEstimate: 0
 47 |           }
 48 |         }),
 49 |       get: jest.fn().mockResolvedValue({
 50 |         data: {
 51 |           id: 'msg-1',
 52 |           threadId: 'thread-1',
 53 |           labelIds: ['INBOX'],
 54 |           snippet: 'Email preview...',
 55 |           payload: {
 56 |             headers: [
 57 |               { name: 'Subject', value: 'Test Subject' },
 58 |               { name: 'From', value: '[email protected]' },
 59 |               { name: 'To', value: '[email protected]' },
 60 |               { name: 'Date', value: new Date().toISOString() }
 61 |             ],
 62 |             body: {
 63 |               data: Buffer.from('Email body content').toString('base64')
 64 |             }
 65 |           }
 66 |         }
 67 |       }),
 68 |       send: jest.fn()
 69 |         .mockResolvedValueOnce({ // Success case
 70 |           data: {
 71 |             id: 'sent-msg-1',
 72 |             threadId: 'thread-1',
 73 |             labelIds: ['SENT']
 74 |           }
 75 |         })
 76 |         .mockRejectedValueOnce(new Error('Send failed')), // Error case
 77 |     },
 78 |     drafts: {
 79 |       create: jest.fn()
 80 |         .mockResolvedValueOnce({ // Success case
 81 |           data: {
 82 |             id: 'draft-1',
 83 |             message: {
 84 |               id: 'msg-draft-1',
 85 |               threadId: 'thread-1',
 86 |               labelIds: ['DRAFT']
 87 |             },
 88 |             updated: new Date().toISOString()
 89 |           }
 90 |         })
 91 |         .mockResolvedValueOnce({ // Reply draft success case
 92 |           data: {
 93 |             id: 'draft-2',
 94 |             message: {
 95 |               id: 'msg-draft-2',
 96 |               threadId: 'thread-1',
 97 |               labelIds: ['DRAFT']
 98 |             },
 99 |             updated: new Date().toISOString()
100 |           }
101 |         })
102 |         .mockRejectedValueOnce(new Error('Draft creation failed')), // Error case
103 |       list: jest.fn()
104 |         .mockResolvedValueOnce({ // Success case with drafts
105 |           data: {
106 |             drafts: [
107 |               { id: 'draft-1' },
108 |               { id: 'draft-2' }
109 |             ],
110 |             nextPageToken: 'next-token',
111 |             resultSizeEstimate: 2
112 |           }
113 |         })
114 |         .mockResolvedValueOnce({ // Empty drafts case
115 |           data: {
116 |             drafts: [],
117 |             resultSizeEstimate: 0
118 |           }
119 |         }),
120 |       get: jest.fn()
121 |         .mockResolvedValue({ // Success case for draft details
122 |           data: {
123 |             id: 'draft-1',
124 |             message: {
125 |               id: 'msg-draft-1',
126 |               threadId: 'thread-1',
127 |               labelIds: ['DRAFT']
128 |             }
129 |           }
130 |         }),
131 |       send: jest.fn()
132 |         .mockResolvedValueOnce({ // Success case
133 |           data: {
134 |             id: 'sent-msg-1',
135 |             threadId: 'thread-1',
136 |             labelIds: ['SENT']
137 |           }
138 |         })
139 |         .mockRejectedValueOnce(new Error('Draft send failed')), // Error case
140 |     },
141 |     getProfile: jest.fn()
142 |       .mockResolvedValueOnce({ // Success case
143 |         data: {
144 |           emailAddress: '[email protected]',
145 |           messagesTotal: 100,
146 |           threadsTotal: 50,
147 |           historyId: '12345'
148 |         }
149 |       })
150 |       .mockRejectedValueOnce(new Error('Settings fetch failed')), // Error case
151 |     settings: {
152 |       getAutoForwarding: jest.fn().mockResolvedValue({
153 |         data: {
154 |           enabled: false,
155 |           emailAddress: undefined
156 |         }
157 |       }),
158 |       getImap: jest.fn().mockResolvedValue({
159 |         data: {
160 |           enabled: true,
161 |           autoExpunge: true,
162 |           expungeBehavior: 'archive'
163 |         }
164 |       }),
165 |       getLanguage: jest.fn().mockResolvedValue({
166 |         data: {
167 |           displayLanguage: 'en'
168 |         }
169 |       }),
170 |       getPop: jest.fn().mockResolvedValue({
171 |         data: {
172 |           enabled: false,
173 |           accessWindow: 'disabled'
174 |         }
175 |       }),
176 |       getVacation: jest.fn().mockResolvedValue({
177 |         data: {
178 |           enabled: false,
179 |           startTime: undefined,
180 |           endTime: undefined,
181 |           responseSubject: '',
182 |           message: ''
183 |         }
184 |       }),
185 |     },
186 |   } as any,
187 | };
188 | 
189 | // Simple file system mock
190 | export const mockFileSystem = () => {
191 |   const fs = {
192 |     mkdir: jest.fn().mockResolvedValue(undefined),
193 |     readFile: jest.fn().mockResolvedValue(Buffer.from('test content')),
194 |     writeFile: jest.fn().mockResolvedValue(undefined),
195 |     unlink: jest.fn().mockResolvedValue(undefined),
196 |     access: jest.fn().mockResolvedValue(undefined),
197 |     chmod: jest.fn().mockResolvedValue(undefined),
198 |   };
199 | 
200 |   jest.mock('fs/promises', () => fs);
201 | 
202 |   return { fs };
203 | };
204 | 
205 | // Mock the account manager module
206 | jest.mock('../modules/accounts/index.js', () => ({
207 |   initializeAccountModule: jest.fn().mockResolvedValue(mockAccountManager()),
208 |   getAccountManager: jest.fn().mockReturnValue(mockAccountManager()),
209 | }));
210 | 
211 | // Mock googleapis
212 | jest.mock('googleapis', () => {
213 |   const mockDrive = {
214 |     files: {
215 |       list: jest.fn(),
216 |       create: jest.fn(),
217 |       get: jest.fn(),
218 |       delete: jest.fn(),
219 |       export: jest.fn(),
220 |     },
221 |     permissions: {
222 |       create: jest.fn(),
223 |     },
224 |   };
225 | 
226 |   return {
227 |     google: {
228 |       gmail: jest.fn().mockReturnValue(mockGmailClient),
229 |       drive: jest.fn().mockReturnValue(mockDrive),
230 |     },
231 |   };
232 | });
233 | 
234 | // Reset mocks between tests
235 | beforeEach(() => {
236 |   jest.clearAllMocks();
237 | });
238 | 
```
--------------------------------------------------------------------------------
/src/modules/attachments/service.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import {
  2 |   AttachmentMetadata,
  3 |   AttachmentResult,
  4 |   AttachmentServiceConfig,
  5 |   AttachmentSource,
  6 |   AttachmentValidationResult,
  7 |   ATTACHMENT_FOLDERS,
  8 |   AttachmentFolderType
  9 | } from './types.js';
 10 | import fs from 'fs/promises';
 11 | import path from 'path';
 12 | import { v4 as uuidv4 } from 'uuid';
 13 | 
 14 | const DEFAULT_CONFIG: AttachmentServiceConfig = {
 15 |   maxSizeBytes: 25 * 1024 * 1024, // 25MB
 16 |   allowedMimeTypes: ['*/*'],
 17 |   quotaLimitBytes: 1024 * 1024 * 1024, // 1GB
 18 |   basePath: process.env.WORKSPACE_BASE_PATH ? 
 19 |     path.join(process.env.WORKSPACE_BASE_PATH, ATTACHMENT_FOLDERS.ROOT) : 
 20 |     '/app/workspace/attachments'
 21 | };
 22 | 
 23 | export class AttachmentService {
 24 |   private static instance: AttachmentService;
 25 |   private config: AttachmentServiceConfig;
 26 |   private initialized = false;
 27 | 
 28 |   private constructor(config: AttachmentServiceConfig = {}) {
 29 |     this.config = { ...DEFAULT_CONFIG, ...config };
 30 |   }
 31 | 
 32 |   /**
 33 |    * Get the singleton instance
 34 |    */
 35 |   public static getInstance(config: AttachmentServiceConfig = {}): AttachmentService {
 36 |     if (!AttachmentService.instance) {
 37 |       AttachmentService.instance = new AttachmentService(config);
 38 |     }
 39 |     return AttachmentService.instance;
 40 |   }
 41 | 
 42 |   /**
 43 |    * Initialize attachment folders in local storage
 44 |    */
 45 |   async initialize(email: string): Promise<void> {
 46 |     try {
 47 |       // Create base attachment directory
 48 |       await fs.mkdir(this.config.basePath!, { recursive: true });
 49 | 
 50 |       // Create email directory structure
 51 |       const emailPath = path.join(this.config.basePath!, ATTACHMENT_FOLDERS.EMAIL);
 52 |       await fs.mkdir(emailPath, { recursive: true });
 53 |       await fs.mkdir(path.join(emailPath, ATTACHMENT_FOLDERS.INCOMING), { recursive: true });
 54 |       await fs.mkdir(path.join(emailPath, ATTACHMENT_FOLDERS.OUTGOING), { recursive: true });
 55 | 
 56 |       // Create calendar directory structure
 57 |       const calendarPath = path.join(this.config.basePath!, ATTACHMENT_FOLDERS.CALENDAR);
 58 |       await fs.mkdir(calendarPath, { recursive: true });
 59 |       await fs.mkdir(path.join(calendarPath, ATTACHMENT_FOLDERS.EVENT_FILES), { recursive: true });
 60 | 
 61 |       this.initialized = true;
 62 |     } catch (error) {
 63 |       throw new Error(`Failed to initialize attachment directories: ${error instanceof Error ? error.message : 'Unknown error'}`);
 64 |     }
 65 |   }
 66 | 
 67 |   /**
 68 |    * Validate attachment against configured limits
 69 |    */
 70 |   private validateAttachment(source: AttachmentSource): AttachmentValidationResult {
 71 |     // Check size if available
 72 |     if (source.metadata.size && this.config.maxSizeBytes) {
 73 |       if (source.metadata.size > this.config.maxSizeBytes) {
 74 |         return {
 75 |           valid: false,
 76 |           error: `File size ${source.metadata.size} exceeds maximum allowed size ${this.config.maxSizeBytes}`
 77 |         };
 78 |       }
 79 |     }
 80 | 
 81 |     // Check MIME type if restricted
 82 |     if (this.config.allowedMimeTypes && 
 83 |         this.config.allowedMimeTypes[0] !== '*/*' &&
 84 |         !this.config.allowedMimeTypes.includes(source.metadata.mimeType)) {
 85 |       return {
 86 |         valid: false,
 87 |         error: `MIME type ${source.metadata.mimeType} is not allowed`
 88 |       };
 89 |     }
 90 | 
 91 |     return { valid: true };
 92 |   }
 93 | 
 94 |   /**
 95 |    * Process attachment and store in local filesystem
 96 |    */
 97 |   async processAttachment(
 98 |     email: string,
 99 |     source: AttachmentSource,
100 |     parentFolder: string
101 |   ): Promise<AttachmentResult> {
102 |     if (!this.initialized) {
103 |       await this.initialize(email);
104 |     }
105 | 
106 |     // Validate attachment
107 |     const validation = this.validateAttachment(source);
108 |     if (!validation.valid) {
109 |       return {
110 |         success: false,
111 |         error: validation.error
112 |       };
113 |     }
114 | 
115 |     try {
116 |       if (!source.content) {
117 |         throw new Error('File content not provided');
118 |       }
119 | 
120 |       // Generate unique ID and create file path
121 |       const id = uuidv4();
122 |       const folderPath = path.join(this.config.basePath!, parentFolder);
123 |       const filePath = path.join(folderPath, `${id}_${source.metadata.name}`);
124 | 
125 |       // Write file content
126 |       const content = Buffer.from(source.content, 'base64');
127 |       await fs.writeFile(filePath, content);
128 | 
129 |       // Get actual file size
130 |       const stats = await fs.stat(filePath);
131 | 
132 |       return {
133 |         success: true,
134 |         attachment: {
135 |           id,
136 |           name: source.metadata.name,
137 |           mimeType: source.metadata.mimeType,
138 |           size: stats.size,
139 |           path: filePath
140 |         }
141 |       };
142 |     } catch (error) {
143 |       return {
144 |         success: false,
145 |         error: error instanceof Error ? error.message : 'Unknown error occurred'
146 |       };
147 |     }
148 |   }
149 | 
150 |   /**
151 |    * Download attachment from local storage
152 |    */
153 |   async downloadAttachment(
154 |     email: string,
155 |     attachmentId: string,
156 |     filePath: string
157 |   ): Promise<AttachmentResult> {
158 |     if (!this.initialized) {
159 |       await this.initialize(email);
160 |     }
161 | 
162 |     try {
163 |       // Verify file exists and is within workspace
164 |       if (!filePath.startsWith(this.config.basePath!)) {
165 |         throw new Error('Invalid file path');
166 |       }
167 | 
168 |       const content = await fs.readFile(filePath);
169 |       const stats = await fs.stat(filePath);
170 | 
171 |       return {
172 |         success: true,
173 |         attachment: {
174 |           id: attachmentId,
175 |           name: path.basename(filePath).substring(37), // Remove UUID prefix
176 |           mimeType: path.extname(filePath) ? 
177 |             `application/${path.extname(filePath).substring(1)}` : 
178 |             'application/octet-stream',
179 |           size: stats.size,
180 |           path: filePath
181 |         }
182 |       };
183 |     } catch (error) {
184 |       return {
185 |         success: false,
186 |         error: error instanceof Error ? error.message : 'Unknown error occurred'
187 |       };
188 |     }
189 |   }
190 | 
191 |   /**
192 |    * Delete attachment from local storage
193 |    */
194 |   async deleteAttachment(
195 |     email: string,
196 |     attachmentId: string,
197 |     filePath: string
198 |   ): Promise<AttachmentResult> {
199 |     if (!this.initialized) {
200 |       await this.initialize(email);
201 |     }
202 | 
203 |     try {
204 |       // Verify file exists and is within workspace
205 |       if (!filePath.startsWith(this.config.basePath!)) {
206 |         throw new Error('Invalid file path');
207 |       }
208 | 
209 |       // Get file stats before deletion
210 |       const stats = await fs.stat(filePath);
211 |       const name = path.basename(filePath).substring(37); // Remove UUID prefix
212 |       const mimeType = path.extname(filePath) ? 
213 |         `application/${path.extname(filePath).substring(1)}` : 
214 |         'application/octet-stream';
215 | 
216 |       // Delete the file
217 |       await fs.unlink(filePath);
218 | 
219 |       return {
220 |         success: true,
221 |         attachment: {
222 |           id: attachmentId,
223 |           name,
224 |           mimeType,
225 |           size: stats.size,
226 |           path: filePath
227 |         }
228 |       };
229 |     } catch (error) {
230 |       return {
231 |         success: false,
232 |         error: error instanceof Error ? error.message : 'Unknown error occurred'
233 |       };
234 |     }
235 |   }
236 | 
237 |   /**
238 |    * Get full path for a specific attachment category
239 |    */
240 |   getAttachmentPath(folder: AttachmentFolderType): string {
241 |     return path.join(this.config.basePath!, folder);
242 |   }
243 | }
244 | 
```
--------------------------------------------------------------------------------
/src/tools/calendar-handlers.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { CalendarService } from '../modules/calendar/service.js';
  2 | import { DriveService } from '../modules/drive/service.js';
  3 | import { validateEmail } from '../utils/account.js';
  4 | import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
  5 | import { getAccountManager } from '../modules/accounts/index.js';
  6 | import { CalendarError } from '../modules/calendar/types.js';
  7 | 
  8 | // Singleton instances
  9 | let driveService: DriveService;
 10 | let calendarService: CalendarService;
 11 | let accountManager: ReturnType<typeof getAccountManager>;
 12 | 
 13 | const CALENDAR_CONFIG = {
 14 |   maxAttachmentSize: 10 * 1024 * 1024, // 10MB
 15 |   allowedAttachmentTypes: [
 16 |     'application/pdf',
 17 |     'application/msword',
 18 |     'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
 19 |     'application/vnd.ms-excel',
 20 |     'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
 21 |     'application/vnd.ms-powerpoint',
 22 |     'application/vnd.openxmlformats-officedocument.presentationml.presentation',
 23 |     'image/jpeg',
 24 |     'image/png',
 25 |     'text/plain'
 26 |   ]
 27 | };
 28 | 
 29 | // Initialize services lazily
 30 | async function initializeServices() {
 31 |   if (!driveService) {
 32 |     driveService = new DriveService();
 33 |   }
 34 |   if (!calendarService) {
 35 |     calendarService = new CalendarService(CALENDAR_CONFIG);
 36 |     await calendarService.ensureInitialized();
 37 |   }
 38 |   if (!accountManager) {
 39 |     accountManager = getAccountManager();
 40 |   }
 41 | }
 42 | 
 43 | export async function handleListWorkspaceCalendarEvents(params: any) {
 44 |   await initializeServices();
 45 |   const { email, query, maxResults, timeMin, timeMax } = params;
 46 | 
 47 |   if (!email) {
 48 |     throw new McpError(
 49 |       ErrorCode.InvalidParams,
 50 |       'Email address is required'
 51 |     );
 52 |   }
 53 | 
 54 |   validateEmail(email);
 55 | 
 56 |   return accountManager.withTokenRenewal(email, async () => {
 57 |     try {
 58 |       return await calendarService.getEvents({
 59 |         email,
 60 |         query,
 61 |         maxResults,
 62 |         timeMin,
 63 |         timeMax
 64 |       });
 65 |     } catch (error) {
 66 |       throw new McpError(
 67 |         ErrorCode.InternalError,
 68 |         `Failed to list calendar events: ${error instanceof Error ? error.message : 'Unknown error'}`
 69 |       );
 70 |     }
 71 |   });
 72 | }
 73 | 
 74 | export async function handleGetWorkspaceCalendarEvent(params: any) {
 75 |   await initializeServices();
 76 |   const { email, eventId } = params;
 77 | 
 78 |   if (!email) {
 79 |     throw new McpError(
 80 |       ErrorCode.InvalidParams,
 81 |       'Email address is required'
 82 |     );
 83 |   }
 84 | 
 85 |   if (!eventId) {
 86 |     throw new McpError(
 87 |       ErrorCode.InvalidParams,
 88 |       'Event ID is required'
 89 |     );
 90 |   }
 91 | 
 92 |   validateEmail(email);
 93 | 
 94 |   return accountManager.withTokenRenewal(email, async () => {
 95 |     try {
 96 |       return await calendarService.getEvent(email, eventId);
 97 |     } catch (error) {
 98 |       throw new McpError(
 99 |         ErrorCode.InternalError,
100 |         `Failed to get calendar event: ${error instanceof Error ? error.message : 'Unknown error'}`
101 |       );
102 |     }
103 |   });
104 | }
105 | 
106 | export async function handleCreateWorkspaceCalendarEvent(params: any) {
107 |   await initializeServices();
108 |   const { email, summary, description, start, end, attendees, attachments } = params;
109 | 
110 |   if (!email) {
111 |     throw new McpError(
112 |       ErrorCode.InvalidParams,
113 |       'Email address is required'
114 |     );
115 |   }
116 | 
117 |   if (!summary) {
118 |     throw new McpError(
119 |       ErrorCode.InvalidParams,
120 |       'Event summary is required'
121 |     );
122 |   }
123 | 
124 |   if (!start || !start.dateTime) {
125 |     throw new McpError(
126 |       ErrorCode.InvalidParams,
127 |       'Event start time is required'
128 |     );
129 |   }
130 | 
131 |   if (!end || !end.dateTime) {
132 |     throw new McpError(
133 |       ErrorCode.InvalidParams,
134 |       'Event end time is required'
135 |     );
136 |   }
137 | 
138 |   validateEmail(email);
139 |   if (attendees) {
140 |     attendees.forEach((attendee: { email: string }) => validateEmail(attendee.email));
141 |   }
142 | 
143 |   return accountManager.withTokenRenewal(email, async () => {
144 |     try {
145 |       return await calendarService.createEvent({
146 |         email,
147 |         summary,
148 |         description,
149 |         start,
150 |         end,
151 |         attendees,
152 |         attachments: attachments?.map((attachment: {
153 |           driveFileId?: string;
154 |           content?: string;
155 |           name: string;
156 |           mimeType: string;
157 |           size?: number;
158 |         }) => ({
159 |           driveFileId: attachment.driveFileId,
160 |           content: attachment.content,
161 |           name: attachment.name,
162 |           mimeType: attachment.mimeType,
163 |           size: attachment.size
164 |         }))
165 |       });
166 |     } catch (error) {
167 |       throw new McpError(
168 |         ErrorCode.InternalError,
169 |         `Failed to create calendar event: ${error instanceof Error ? error.message : 'Unknown error'}`
170 |       );
171 |     }
172 |   });
173 | }
174 | 
175 | export async function handleManageWorkspaceCalendarEvent(params: any) {
176 |   await initializeServices();
177 |   const { email, eventId, action, comment, newTimes } = params;
178 | 
179 |   if (!email) {
180 |     throw new McpError(
181 |       ErrorCode.InvalidParams,
182 |       'Email address is required'
183 |     );
184 |   }
185 | 
186 |   if (!eventId) {
187 |     throw new McpError(
188 |       ErrorCode.InvalidParams,
189 |       'Event ID is required'
190 |     );
191 |   }
192 | 
193 |   if (!action) {
194 |     throw new McpError(
195 |       ErrorCode.InvalidParams,
196 |       'Action is required'
197 |     );
198 |   }
199 | 
200 |   validateEmail(email);
201 | 
202 |   return accountManager.withTokenRenewal(email, async () => {
203 |     try {
204 |       return await calendarService.manageEvent({
205 |         email,
206 |         eventId,
207 |         action,
208 |         comment,
209 |         newTimes
210 |       });
211 |     } catch (error) {
212 |       throw new McpError(
213 |         ErrorCode.InternalError,
214 |         `Failed to manage calendar event: ${error instanceof Error ? error.message : 'Unknown error'}`
215 |       );
216 |     }
217 |   });
218 | }
219 | 
220 | export async function handleDeleteWorkspaceCalendarEvent(params: any) {
221 |   await initializeServices();
222 |   const { email, eventId, sendUpdates, deletionScope } = params;
223 | 
224 |   if (!email) {
225 |     throw new McpError(
226 |       ErrorCode.InvalidParams,
227 |       'Email address is required'
228 |     );
229 |   }
230 | 
231 |   if (!eventId) {
232 |     throw new McpError(
233 |       ErrorCode.InvalidParams,
234 |       'Event ID is required'
235 |     );
236 |   }
237 | 
238 |   // Validate deletionScope if provided
239 |   if (deletionScope && !['entire_series', 'this_and_following'].includes(deletionScope)) {
240 |     throw new McpError(
241 |       ErrorCode.InvalidParams,
242 |       'Invalid deletion scope. Must be one of: entire_series, this_and_following'
243 |     );
244 |   }
245 | 
246 |   validateEmail(email);
247 | 
248 |   return accountManager.withTokenRenewal(email, async () => {
249 |     try {
250 |       await calendarService.deleteEvent(email, eventId, sendUpdates, deletionScope);
251 |       // Return a success response object instead of void
252 |       return {
253 |         status: 'success',
254 |         message: 'Event deleted successfully',
255 |         details: deletionScope ? 
256 |           `Event deleted with scope: ${deletionScope}` : 
257 |           'Event deleted completely'
258 |       };
259 |     } catch (error) {
260 |       // Check if this is a CalendarError with a specific code
261 |       if (error instanceof CalendarError) {
262 |         throw new McpError(
263 |           ErrorCode.InvalidParams,
264 |           error.message,
265 |           error.details
266 |         );
267 |       }
268 |       
269 |       throw new McpError(
270 |         ErrorCode.InternalError,
271 |         `Failed to delete calendar event: ${error instanceof Error ? error.message : 'Unknown error'}`
272 |       );
273 |     }
274 |   });
275 | }
276 | 
```
--------------------------------------------------------------------------------
/src/tools/types.ts:
--------------------------------------------------------------------------------
```typescript
  1 | /**
  2 |  * Standard response format for all MCP tools
  3 |  * @property content - Array of content blocks to return to the client
  4 |  * @property isError - Whether this response represents an error condition
  5 |  * @property _meta - Optional metadata about the response
  6 |  */
  7 | export interface McpToolResponse {
  8 |   content: {
  9 |     type: 'text';
 10 |     text: string;
 11 |   }[];
 12 |   isError?: boolean;
 13 |   _meta?: Record<string, unknown>;
 14 | }
 15 | 
 16 | /**
 17 |  * Base interface for all tool arguments
 18 |  * Provides type safety for unknown properties
 19 |  */
 20 | export interface ToolArguments {
 21 |   [key: string]: unknown;
 22 | }
 23 | 
 24 | /**
 25 |  * Common arguments required by all workspace tools
 26 |  * @property email - The Google Workspace account email address
 27 |  */
 28 | export interface BaseToolArguments extends Record<string, unknown> {
 29 |   email: string;
 30 | }
 31 | 
 32 | /**
 33 |  * Parameters for authenticating a workspace account
 34 |  * @property email - Email address to authenticate
 35 |  * @property category - Optional account category (e.g., 'work', 'personal')
 36 |  * @property description - Optional account description
 37 |  * @property auth_code - OAuth2 authorization code (required for completing auth)
 38 |  */
 39 | export interface AuthenticateAccountArgs extends BaseToolArguments {
 40 |   category?: string;
 41 |   description?: string;
 42 |   auth_code?: string;
 43 | }
 44 | 
 45 | // Gmail Types
 46 | /**
 47 |  * Parameters for searching Gmail messages
 48 |  * @property email - The Gmail account to search
 49 |  * @property search - Search criteria for filtering emails
 50 |  * @property options - Search options including pagination and format
 51 |  */
 52 | export interface GmailSearchParams {
 53 |   email: string;
 54 |   search?: {
 55 |     from?: string | string[];
 56 |     to?: string | string[];
 57 |     subject?: string;
 58 |     content?: string;
 59 |     after?: string; // YYYY-MM-DD
 60 |     before?: string; // YYYY-MM-DD
 61 |     hasAttachment?: boolean;
 62 |     labels?: string[]; // e.g., ["INBOX", "IMPORTANT"]
 63 |     excludeLabels?: string[];
 64 |     includeSpam?: boolean;
 65 |     isUnread?: boolean;
 66 |   };
 67 |   options?: {
 68 |     maxResults?: number;
 69 |     pageToken?: string;
 70 |     includeHeaders?: boolean;
 71 |     format?: 'full' | 'metadata' | 'minimal';
 72 |     threadedView?: boolean;
 73 |     sortOrder?: 'asc' | 'desc';
 74 |   };
 75 | }
 76 | 
 77 | /**
 78 |  * Content for sending or creating draft emails
 79 |  * @property to - Array of recipient email addresses
 80 |  * @property subject - Email subject line
 81 |  * @property body - Email body content (supports HTML)
 82 |  * @property cc - Optional CC recipients
 83 |  * @property bcc - Optional BCC recipients
 84 |  */
 85 | export interface EmailContent {
 86 |   to: string[];
 87 |   subject: string;
 88 |   body: string;
 89 |   cc?: string[];
 90 |   bcc?: string[];
 91 | }
 92 | 
 93 | /**
 94 |  * Parameters for sending emails
 95 |  */
 96 | export interface SendEmailArgs extends BaseToolArguments, EmailContent {}
 97 | 
 98 | // Calendar Types
 99 | /**
100 |  * Parameters for listing calendar events
101 |  * @property email - Calendar owner's email
102 |  * @property query - Optional text search within events
103 |  * @property maxResults - Maximum events to return
104 |  * @property timeMin - Start of time range (ISO-8601)
105 |  * @property timeMax - End of time range (ISO-8601)
106 |  */
107 | export interface CalendarEventParams extends BaseToolArguments {
108 |   query?: string;
109 |   maxResults?: number; // Default: 10
110 |   timeMin?: string; // ISO-8601 format
111 |   timeMax?: string; // ISO-8601 format
112 | }
113 | 
114 | /**
115 |  * Time specification for calendar events
116 |  * @property dateTime - Event time in ISO-8601 format
117 |  * @property timeZone - IANA timezone identifier
118 |  */
119 | export interface CalendarEventTime {
120 |   dateTime: string; // e.g., "2024-02-18T15:30:00-06:00"
121 |   timeZone?: string; // e.g., "America/Chicago"
122 | }
123 | 
124 | /**
125 |  * Calendar event attendee information
126 |  * @property email - Attendee's email address
127 |  */
128 | export interface CalendarEventAttendee {
129 |   email: string;
130 | }
131 | 
132 | // Drive Types
133 | /**
134 |  * Parameters for listing Drive files
135 |  */
136 | export interface DriveFileListArgs extends BaseToolArguments {
137 |   options?: {
138 |     folderId?: string;
139 |     query?: string;
140 |     pageSize?: number;
141 |     orderBy?: string[];
142 |     fields?: string[];
143 |   };
144 | }
145 | 
146 | /**
147 |  * Parameters for searching Drive files
148 |  */
149 | export interface DriveSearchArgs extends BaseToolArguments {
150 |   options: {
151 |     fullText?: string;
152 |     mimeType?: string;
153 |     folderId?: string;
154 |     trashed?: boolean;
155 |     query?: string;
156 |     pageSize?: number;
157 |   };
158 | }
159 | 
160 | /**
161 |  * Parameters for uploading files to Drive
162 |  */
163 | export interface DriveUploadArgs extends BaseToolArguments {
164 |   options: {
165 |     name: string;
166 |     content: string;
167 |     mimeType?: string;
168 |     parents?: string[];
169 |   };
170 | }
171 | 
172 | /**
173 |  * Parameters for downloading files from Drive
174 |  */
175 | export interface DriveDownloadArgs extends BaseToolArguments {
176 |   fileId: string;
177 |   mimeType?: string;
178 | }
179 | 
180 | /**
181 |  * Parameters for creating Drive folders
182 |  */
183 | export interface DriveFolderArgs extends BaseToolArguments {
184 |   name: string;
185 |   parentId?: string;
186 | }
187 | 
188 | /**
189 |  * Parameters for updating Drive permissions
190 |  */
191 | export interface DrivePermissionArgs extends BaseToolArguments {
192 |   options: {
193 |     fileId: string;
194 |     role: 'owner' | 'organizer' | 'fileOrganizer' | 'writer' | 'commenter' | 'reader';
195 |     type: 'user' | 'group' | 'domain' | 'anyone';
196 |     emailAddress?: string;
197 |     domain?: string;
198 |     allowFileDiscovery?: boolean;
199 |   };
200 | }
201 | 
202 | /**
203 |  * Parameters for deleting Drive files
204 |  */
205 | export interface DriveDeleteArgs extends BaseToolArguments {
206 |   fileId: string;
207 | }
208 | 
209 | // Label Types
210 | /**
211 |  * Color settings for Gmail labels
212 |  * @property textColor - Hex color code for label text
213 |  * @property backgroundColor - Hex color code for label background
214 |  */
215 | export interface LabelColor {
216 |   textColor: string; // e.g., "#000000"
217 |   backgroundColor: string; // e.g., "#FFFFFF"
218 | }
219 | 
220 | /**
221 |  * Parameters for creating labels
222 |  */
223 | export interface CreateLabelArgs extends BaseToolArguments {
224 |   name: string;
225 |   messageListVisibility?: 'show' | 'hide';
226 |   labelListVisibility?: 'labelShow' | 'labelHide' | 'labelShowIfUnread';
227 |   color?: LabelColor;
228 | }
229 | 
230 | /**
231 |  * Parameters for updating labels
232 |  */
233 | export interface UpdateLabelArgs extends CreateLabelArgs {
234 |   labelId: string;
235 | }
236 | 
237 | /**
238 |  * Parameters for deleting labels
239 |  */
240 | export interface DeleteLabelArgs extends BaseToolArguments {
241 |   labelId: string;
242 | }
243 | 
244 | /**
245 |  * Parameters for modifying message labels
246 |  */
247 | export interface ModifyLabelsArgs extends BaseToolArguments {
248 |   messageId: string;
249 |   addLabelIds?: string[];
250 |   removeLabelIds?: string[];
251 | }
252 | 
253 | // Label Filter Types
254 | /**
255 |  * Filter criteria for matching emails
256 |  */
257 | export interface LabelFilterCriteria {
258 |   from?: string[];
259 |   to?: string[];
260 |   subject?: string;
261 |   hasWords?: string[];
262 |   doesNotHaveWords?: string[];
263 |   hasAttachment?: boolean;
264 |   size?: {
265 |     operator: 'larger' | 'smaller';
266 |     size: number;
267 |   };
268 | }
269 | 
270 | /**
271 |  * Actions to take when filter matches
272 |  */
273 | export interface LabelFilterActions {
274 |   addLabel: boolean;
275 |   markImportant?: boolean;
276 |   markRead?: boolean;
277 |   archive?: boolean;
278 | }
279 | 
280 | /**
281 |  * Parameters for creating label filters
282 |  */
283 | export interface CreateLabelFilterArgs extends BaseToolArguments {
284 |   labelId: string;
285 |   criteria: LabelFilterCriteria;
286 |   actions: LabelFilterActions;
287 | }
288 | 
289 | /**
290 |  * Parameters for getting label filters
291 |  */
292 | export interface GetLabelFiltersArgs extends BaseToolArguments {
293 |   labelId?: string;  // Optional: get filters for specific label
294 | }
295 | 
296 | /**
297 |  * Parameters for updating label filters
298 |  */
299 | export interface UpdateLabelFilterArgs extends BaseToolArguments {
300 |   filterId: string;
301 |   criteria?: LabelFilterCriteria;
302 |   actions?: LabelFilterActions;
303 | }
304 | 
305 | /**
306 |  * Parameters for deleting label filters
307 |  */
308 | export interface DeleteLabelFilterArgs extends BaseToolArguments {
309 |   filterId: string;
310 | }
311 | 
312 | // Attachment Management Types
313 | export interface ManageAttachmentParams extends BaseToolArguments {
314 |   action: 'download' | 'upload' | 'delete';
315 |   source: 'email' | 'calendar';
316 |   messageId: string;
317 |   filename: string;
318 |   content?: string;  // Required for upload action
319 | }
320 | 
321 | // Re-export consolidated management types
322 | export {
323 |   ManageLabelParams,
324 |   ManageLabelAssignmentParams,
325 |   ManageLabelFilterParams
326 | } from '../modules/gmail/services/label.js';
327 | 
328 | export {
329 |   ManageDraftParams,
330 |   DraftAction
331 | } from '../modules/gmail/services/draft.js';
332 | 
```
--------------------------------------------------------------------------------
/src/modules/drive/__tests__/service.test.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { DriveService } from '../service.js';
  2 | import { getAccountManager } from '../../accounts/index.js';
  3 | import { DRIVE_SCOPES } from '../scopes.js';
  4 | import { GoogleServiceError } from '../../../services/base/BaseGoogleService.js';
  5 | import { mockFileSystem } from '../../../__helpers__/testSetup.js';
  6 | 
  7 | jest.mock('../../accounts/index.js');
  8 | jest.mock('googleapis');
  9 | jest.mock('../../../utils/workspace.js', () => ({
 10 |   workspaceManager: {
 11 |     getUploadPath: jest.fn().mockResolvedValue('/tmp/test-upload.txt'),
 12 |     getDownloadPath: jest.fn().mockResolvedValue('/tmp/test-download.txt'),
 13 |     initializeAccountDirectories: jest.fn().mockResolvedValue(undefined)
 14 |   }
 15 | }));
 16 | 
 17 | const { fs } = mockFileSystem();
 18 | 
 19 | describe('DriveService', () => {
 20 |   const testEmail = '[email protected]';
 21 |   let service: DriveService;
 22 |   let mockDrive: any;
 23 | 
 24 |   beforeEach(async () => {
 25 |     jest.clearAllMocks();
 26 | 
 27 |     // Mock file system operations
 28 |     fs.writeFile.mockResolvedValue(undefined);
 29 |     fs.readFile.mockResolvedValue(Buffer.from('test content'));
 30 |     
 31 |     // Mock account manager
 32 |     (getAccountManager as jest.Mock).mockReturnValue({
 33 |       validateToken: jest.fn().mockResolvedValue({
 34 |         valid: true,
 35 |         token: { access_token: 'test-token' },
 36 |         requiredScopes: Object.values(DRIVE_SCOPES)
 37 |       }),
 38 |       getAuthClient: jest.fn().mockResolvedValue({
 39 |         setCredentials: jest.fn()
 40 |       })
 41 |     });
 42 | 
 43 |     const { google } = require('googleapis');
 44 |     mockDrive = {
 45 |       files: {
 46 |         list: jest.fn(),
 47 |         create: jest.fn(),
 48 |         get: jest.fn(),
 49 |         delete: jest.fn(),
 50 |         export: jest.fn()
 51 |       },
 52 |       permissions: {
 53 |         create: jest.fn()
 54 |       }
 55 |     };
 56 |     google.drive.mockReturnValue(mockDrive);
 57 |     
 58 |     service = new DriveService();
 59 |   });
 60 | 
 61 |   describe('listFiles', () => {
 62 |     it('should list files successfully', async () => {
 63 |       const mockResponse = {
 64 |         data: {
 65 |           files: [
 66 |             { id: '1', name: 'test.txt' }
 67 |           ]
 68 |         }
 69 |       };
 70 | 
 71 |       mockDrive.files.list.mockResolvedValue(mockResponse);
 72 | 
 73 |       const result = await service.listFiles(testEmail);
 74 | 
 75 |       expect(result.success).toBe(true);
 76 |       expect(result.data).toEqual(mockResponse.data);
 77 |       expect(mockDrive.files.list).toHaveBeenCalled();
 78 |     });
 79 | 
 80 |     it('should handle errors', async () => {
 81 |       mockDrive.files.list.mockRejectedValue(new Error('API error'));
 82 | 
 83 |       const result = await service.listFiles(testEmail);
 84 | 
 85 |       expect(result.success).toBe(false);
 86 |       expect(result.error).toBe('API error');
 87 |     });
 88 |   });
 89 | 
 90 |   describe('uploadFile', () => {
 91 |     it('should upload file successfully', async () => {
 92 |       const mockResponse = {
 93 |         data: {
 94 |           id: '1',
 95 |           name: 'test.txt',
 96 |           mimeType: 'text/plain',
 97 |           webViewLink: 'https://drive.google.com/file/d/1'
 98 |         }
 99 |       };
100 | 
101 |       mockDrive.files.create.mockResolvedValue(mockResponse);
102 | 
103 |       const result = await service.uploadFile(testEmail, {
104 |         name: 'test.txt',
105 |         content: 'test content',
106 |         mimeType: 'text/plain'
107 |       });
108 | 
109 |       expect(result.success).toBe(true);
110 |       expect(result.data).toEqual(mockResponse.data);
111 |       expect(mockDrive.files.create).toHaveBeenCalledWith(expect.objectContaining({
112 |         requestBody: {
113 |           name: 'test.txt',
114 |           mimeType: 'text/plain'
115 |         },
116 |         fields: 'id, name, mimeType, webViewLink'
117 |       }));
118 |     });
119 | 
120 |     it('should handle upload errors', async () => {
121 |       mockDrive.files.create.mockRejectedValue(new Error('Upload failed'));
122 | 
123 |       const result = await service.uploadFile(testEmail, {
124 |         name: 'test.txt',
125 |         content: 'test content'
126 |       });
127 | 
128 |       expect(result.success).toBe(false);
129 |       expect(result.error).toBe('Upload failed');
130 |     });
131 |   });
132 | 
133 |   describe('downloadFile', () => {
134 |     // Simplified to a single test case
135 |     it('should handle download operations', async () => {
136 |       const mockMetadata = {
137 |         data: {
138 |           name: 'test.txt',
139 |           mimeType: 'text/plain'
140 |         }
141 |       };
142 |       mockDrive.files.get.mockResolvedValue(mockMetadata);
143 | 
144 |       const result = await service.downloadFile(testEmail, {
145 |         fileId: '1'
146 |       });
147 | 
148 |       expect(result.success).toBeDefined();
149 |       expect(mockDrive.files.get).toHaveBeenCalled();
150 |     });
151 |   });
152 | 
153 |   describe('searchFiles', () => {
154 |     // Simplified to basic functionality test
155 |     it('should handle search operations', async () => {
156 |       const mockResponse = {
157 |         data: {
158 |           files: [{ id: '1', name: 'test.txt' }]
159 |         }
160 |       };
161 |       mockDrive.files.list.mockResolvedValue(mockResponse);
162 | 
163 |       const result = await service.searchFiles(testEmail, {
164 |         fullText: 'test'
165 |       });
166 | 
167 |       expect(result.success).toBeDefined();
168 |       expect(mockDrive.files.list).toHaveBeenCalled();
169 |     });
170 |   });
171 | 
172 |   describe('updatePermissions', () => {
173 |     it('should update permissions successfully', async () => {
174 |       const mockResponse = {
175 |         data: {
176 |           id: '1',
177 |           type: 'user',
178 |           role: 'reader'
179 |         }
180 |       };
181 | 
182 |       mockDrive.permissions.create.mockResolvedValue(mockResponse);
183 | 
184 |       const result = await service.updatePermissions(testEmail, {
185 |         fileId: '1',
186 |         type: 'user',
187 |         role: 'reader',
188 |         emailAddress: '[email protected]'
189 |       });
190 | 
191 |       expect(result.success).toBe(true);
192 |       expect(result.data).toEqual(mockResponse.data);
193 |       expect(mockDrive.permissions.create).toHaveBeenCalledWith({
194 |         fileId: '1',
195 |         requestBody: {
196 |           role: 'reader',
197 |           type: 'user',
198 |           emailAddress: '[email protected]'
199 |         }
200 |       });
201 |     });
202 | 
203 |     it('should handle permission update errors', async () => {
204 |       mockDrive.permissions.create.mockRejectedValue(new Error('Permission update failed'));
205 | 
206 |       const result = await service.updatePermissions(testEmail, {
207 |         fileId: '1',
208 |         type: 'user',
209 |         role: 'reader',
210 |         emailAddress: '[email protected]'
211 |       });
212 | 
213 |       expect(result.success).toBe(false);
214 |       expect(result.error).toBe('Permission update failed');
215 |     });
216 |   });
217 | 
218 |   describe('deleteFile', () => {
219 |     it('should delete file successfully', async () => {
220 |       mockDrive.files.delete.mockResolvedValue({});
221 | 
222 |       const result = await service.deleteFile(testEmail, '1');
223 | 
224 |       expect(result.success).toBe(true);
225 |       expect(mockDrive.files.delete).toHaveBeenCalledWith({
226 |         fileId: '1'
227 |       });
228 |     });
229 | 
230 |     it('should handle delete errors', async () => {
231 |       mockDrive.files.delete.mockRejectedValue(new Error('Delete failed'));
232 | 
233 |       const result = await service.deleteFile(testEmail, '1');
234 | 
235 |       expect(result.success).toBe(false);
236 |       expect(result.error).toBe('Delete failed');
237 |     });
238 |   });
239 | 
240 |   describe('createFolder', () => {
241 |     it('should create folder successfully', async () => {
242 |       const mockResponse = {
243 |         data: {
244 |           id: '1',
245 |           name: 'Test Folder',
246 |           mimeType: 'application/vnd.google-apps.folder',
247 |           webViewLink: 'https://drive.google.com/drive/folders/1'
248 |         }
249 |       };
250 | 
251 |       mockDrive.files.create.mockResolvedValue(mockResponse);
252 | 
253 |       const result = await service.createFolder(testEmail, 'Test Folder', 'parent123');
254 | 
255 |       expect(result.success).toBe(true);
256 |       expect(result.data).toEqual(mockResponse.data);
257 |       expect(mockDrive.files.create).toHaveBeenCalledWith({
258 |         requestBody: {
259 |           name: 'Test Folder',
260 |           mimeType: 'application/vnd.google-apps.folder',
261 |           parents: ['parent123']
262 |         },
263 |         fields: 'id, name, mimeType, webViewLink'
264 |       });
265 |     });
266 | 
267 |     it('should handle folder creation errors', async () => {
268 |       mockDrive.files.create.mockRejectedValue(new Error('Folder creation failed'));
269 | 
270 |       const result = await service.createFolder(testEmail, 'Test Folder');
271 | 
272 |       expect(result.success).toBe(false);
273 |       expect(result.error).toBe('Folder creation failed');
274 |     });
275 |   });
276 | });
277 | 
```
--------------------------------------------------------------------------------
/src/__tests__/modules/gmail/service.test.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { GmailService } from '../../../modules/gmail/service.js';
  2 | import { gmail_v1 } from 'googleapis';
  3 | import { getAccountManager } from '../../../modules/accounts/index.js';
  4 | import { AccountManager } from '../../../modules/accounts/manager.js';
  5 | import { DraftResponse, GetDraftsResponse, SendEmailResponse } from '../../../modules/gmail/types.js';
  6 | 
  7 | jest.mock('../../../modules/accounts/index.js');
  8 | jest.mock('../../../modules/accounts/manager.js');
  9 | 
 10 | describe('GmailService', () => {
 11 |   let gmailService: GmailService;
 12 |   let mockGmailClient: jest.Mocked<gmail_v1.Gmail>;
 13 |   let mockAccountManager: jest.Mocked<AccountManager>;
 14 |   const testEmail = '[email protected]';
 15 | 
 16 |   beforeEach(async () => {
 17 |     // Simplified mock setup
 18 |     mockGmailClient = {
 19 |       users: {
 20 |         messages: {
 21 |           list: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
 22 |           get: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
 23 |           send: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
 24 |         },
 25 |         drafts: {
 26 |           create: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
 27 |           list: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
 28 |           get: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
 29 |           send: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
 30 |         },
 31 |         getProfile: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
 32 |         settings: {
 33 |           getAutoForwarding: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
 34 |           getImap: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
 35 |           getLanguage: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
 36 |           getPop: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
 37 |           getVacation: jest.fn().mockImplementation(() => Promise.resolve({ data: {} })),
 38 |         },
 39 |       },
 40 |     } as any;
 41 | 
 42 |     mockAccountManager = {
 43 |       validateToken: jest.fn().mockResolvedValue({ valid: true, token: {} }),
 44 |       getAuthClient: jest.fn().mockResolvedValue({}),
 45 |       withTokenRenewal: jest.fn().mockImplementation((email, operation) => operation()),
 46 |     } as unknown as jest.Mocked<AccountManager>;
 47 | 
 48 |     (getAccountManager as jest.Mock).mockReturnValue(mockAccountManager);
 49 | 
 50 |     gmailService = new GmailService();
 51 |     await gmailService.initialize();
 52 | 
 53 |     // Mock the base service's getAuthenticatedClient method
 54 |     (gmailService as any).getAuthenticatedClient = jest.fn().mockResolvedValue(mockGmailClient);
 55 | 
 56 |     // Mock all internal services
 57 |     (gmailService as any).emailService.gmailClient = mockGmailClient;
 58 |     (gmailService as any).emailService.getAuthenticatedClient = jest.fn().mockResolvedValue(mockGmailClient);
 59 |     
 60 |     (gmailService as any).draftService.gmailClient = mockGmailClient;
 61 |     (gmailService as any).draftService.getAuthenticatedClient = jest.fn().mockResolvedValue(mockGmailClient);
 62 |     
 63 |     (gmailService as any).settingsService.gmailClient = mockGmailClient;
 64 |     (gmailService as any).settingsService.getAuthenticatedClient = jest.fn().mockResolvedValue(mockGmailClient);
 65 |   });
 66 | 
 67 |   describe('getEmails', () => {
 68 |     it('should get emails with search criteria', async () => {
 69 |       const mockMessages = [
 70 |         { id: 'msg1', threadId: 'thread1' },
 71 |         { id: 'msg2', threadId: 'thread2' }
 72 |       ];
 73 | 
 74 |       (mockGmailClient.users.messages.list as jest.Mock).mockImplementation(() =>
 75 |         Promise.resolve({ data: { messages: mockMessages, resultSizeEstimate: 2 } })
 76 |       );
 77 | 
 78 |       const result = await gmailService.getEmails({
 79 |         email: testEmail,
 80 |         search: { subject: 'test' }
 81 |       });
 82 | 
 83 |       expect(result.emails.length).toBe(2);
 84 |       expect(result.resultSummary.total).toBe(2);
 85 |     });
 86 | 
 87 |     it('should handle empty results', async () => {
 88 |       (mockGmailClient.users.messages.list as jest.Mock).mockImplementation(() =>
 89 |         Promise.resolve({ data: { messages: [], resultSizeEstimate: 0 } })
 90 |       );
 91 | 
 92 |       const result = await gmailService.getEmails({ email: testEmail });
 93 |       expect(result.emails).toEqual([]);
 94 |       expect(result.resultSummary.total).toBe(0);
 95 |     });
 96 |   });
 97 | 
 98 |   describe('sendEmail', () => {
 99 |     const emailParams = {
100 |       email: testEmail,
101 |       to: ['[email protected]'],
102 |       subject: 'Test',
103 |       body: 'Hello'
104 |     };
105 | 
106 |     it('should send email', async () => {
107 |       (mockGmailClient.users.messages.send as jest.Mock).mockImplementation(() =>
108 |         Promise.resolve({
109 |           data: { id: 'msg1', threadId: 'thread1', labelIds: ['SENT'] }
110 |         })
111 |       );
112 | 
113 |       const result = await gmailService.sendEmail(emailParams);
114 |       expect(result.messageId).toBe('msg1');
115 |       expect(result.labelIds).toContain('SENT');
116 |     });
117 | 
118 |     it('should handle send failure', async () => {
119 |       (mockGmailClient.users.messages.send as jest.Mock).mockImplementation(() =>
120 |         Promise.reject(new Error('Send failed'))
121 |       );
122 | 
123 |       await expect(gmailService.sendEmail(emailParams)).rejects.toThrow();
124 |     });
125 |   });
126 | 
127 |   describe('manageDraft', () => {
128 |     it('should create draft', async () => {
129 |       (mockGmailClient.users.drafts.create as jest.Mock).mockImplementation(() =>
130 |         Promise.resolve({
131 |           data: {
132 |             id: 'draft1',
133 |             message: { id: 'msg1' }
134 |           }
135 |         })
136 |       );
137 | 
138 |       const result = await gmailService.manageDraft({
139 |         action: 'create',
140 |         email: testEmail,
141 |         data: {
142 |           to: ['[email protected]'],
143 |           subject: 'Draft',
144 |           body: 'Content'
145 |         }
146 |       }) as DraftResponse;
147 | 
148 |       expect(result).toHaveProperty('id', 'draft1');
149 |     });
150 | 
151 |     it('should list drafts', async () => {
152 |       // Mock the list call to return draft IDs
153 |       (mockGmailClient.users.drafts.list as jest.Mock).mockImplementation(() =>
154 |         Promise.resolve({
155 |           data: {
156 |             drafts: [
157 |               { id: 'draft1' },
158 |               { id: 'draft2' }
159 |             ],
160 |             resultSizeEstimate: 2
161 |           }
162 |         })
163 |       );
164 | 
165 |       // Mock the get call for each draft
166 |       (mockGmailClient.users.drafts.get as jest.Mock)
167 |         .mockImplementationOnce(() => Promise.resolve({
168 |           data: {
169 |             id: 'draft1',
170 |             message: {
171 |               id: 'msg1',
172 |               threadId: 'thread1',
173 |               labelIds: ['DRAFT']
174 |             }
175 |           }
176 |         }))
177 |         .mockImplementationOnce(() => Promise.resolve({
178 |           data: {
179 |             id: 'draft2',
180 |             message: {
181 |               id: 'msg2',
182 |               threadId: 'thread2',
183 |               labelIds: ['DRAFT']
184 |             }
185 |           }
186 |         }));
187 | 
188 |       const result = await gmailService.manageDraft({
189 |         action: 'read',
190 |         email: testEmail
191 |       }) as GetDraftsResponse;
192 | 
193 |       expect(result).toHaveProperty('drafts');
194 |       expect(result.drafts.length).toBe(2);
195 |       expect(result.drafts[0]).toHaveProperty('id', 'draft1');
196 |       expect(result.drafts[1]).toHaveProperty('id', 'draft2');
197 |     });
198 | 
199 |     it('should send draft', async () => {
200 |       (mockGmailClient.users.drafts.send as jest.Mock).mockImplementation(() =>
201 |         Promise.resolve({
202 |           data: {
203 |             id: 'msg1',
204 |             threadId: 'thread1',
205 |             labelIds: ['SENT']
206 |           }
207 |         })
208 |       );
209 | 
210 |       const result = await gmailService.manageDraft({
211 |         action: 'send',
212 |         email: testEmail,
213 |         draftId: 'draft1'
214 |       }) as SendEmailResponse;
215 | 
216 |       expect(result).toHaveProperty('messageId', 'msg1');
217 |       expect(result).toHaveProperty('labelIds');
218 |     });
219 |   });
220 | 
221 |   describe('getWorkspaceGmailSettings', () => {
222 |     it('should get settings', async () => {
223 |       (mockGmailClient.users.getProfile as jest.Mock).mockImplementation(() =>
224 |         Promise.resolve({
225 |           data: {
226 |             emailAddress: testEmail,
227 |             messagesTotal: 100
228 |           }
229 |         })
230 |       );
231 | 
232 |       const result = await gmailService.getWorkspaceGmailSettings({
233 |         email: testEmail
234 |       });
235 | 
236 |       expect(result.profile.emailAddress).toBe(testEmail);
237 |       expect(result.settings).toBeDefined();
238 |     });
239 | 
240 |     it('should handle settings fetch error', async () => {
241 |       (mockGmailClient.users.getProfile as jest.Mock).mockImplementation(() =>
242 |         Promise.reject(new Error('Failed to fetch'))
243 |       );
244 | 
245 |       await expect(gmailService.getWorkspaceGmailSettings({
246 |         email: testEmail
247 |       })).rejects.toThrow();
248 |     });
249 |   });
250 | });
251 | 
```
--------------------------------------------------------------------------------
/src/modules/tools/registry.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { Tool } from "@modelcontextprotocol/sdk/types.js";
  2 | 
  3 | export interface ToolMetadata extends Tool {
  4 |   category: string;
  5 |   aliases?: string[];
  6 | }
  7 | 
  8 | export interface ToolCategory {
  9 |   name: string;
 10 |   description: string;
 11 |   tools: ToolMetadata[];
 12 | }
 13 | 
 14 | export class ToolRegistry {
 15 |   private tools: Map<string, ToolMetadata> = new Map();
 16 |   private categories: Map<string, ToolCategory> = new Map();
 17 |   private aliasMap: Map<string, string> = new Map();
 18 | 
 19 |   constructor(tools: ToolMetadata[]) {
 20 |     this.registerTools(tools);
 21 |   }
 22 | 
 23 |   private registerTools(tools: ToolMetadata[]): void {
 24 |     for (const tool of tools) {
 25 |       // Register the main tool
 26 |       this.tools.set(tool.name, tool);
 27 | 
 28 |       // Register category
 29 |       if (!this.categories.has(tool.category)) {
 30 |         this.categories.set(tool.category, {
 31 |           name: tool.category,
 32 |           description: '', // Could be added in future
 33 |           tools: []
 34 |         });
 35 |       }
 36 |       this.categories.get(tool.category)?.tools.push(tool);
 37 | 
 38 |       // Register aliases
 39 |       if (tool.aliases) {
 40 |         for (const alias of tool.aliases) {
 41 |           this.aliasMap.set(alias, tool.name);
 42 |         }
 43 |       }
 44 |     }
 45 |   }
 46 | 
 47 |   getTool(name: string): ToolMetadata | undefined {
 48 |     // Try direct lookup
 49 |     const tool = this.tools.get(name);
 50 |     if (tool) {
 51 |       return tool;
 52 |     }
 53 | 
 54 |     // Try alias lookup
 55 |     const mainName = this.aliasMap.get(name);
 56 |     if (mainName) {
 57 |       return this.tools.get(mainName);
 58 |     }
 59 | 
 60 |     return undefined;
 61 |   }
 62 | 
 63 |   getAllTools(): ToolMetadata[] {
 64 |     return Array.from(this.tools.values());
 65 |   }
 66 | 
 67 |   getCategories(): ToolCategory[] {
 68 |     return Array.from(this.categories.values());
 69 |   }
 70 | 
 71 |   private calculateLevenshteinDistance(a: string, b: string): number {
 72 |     const matrix: number[][] = [];
 73 | 
 74 |     // Initialize matrix
 75 |     for (let i = 0; i <= b.length; i++) {
 76 |       matrix[i] = [i];
 77 |     }
 78 |     for (let j = 0; j <= a.length; j++) {
 79 |       matrix[0][j] = j;
 80 |     }
 81 | 
 82 |     // Fill matrix
 83 |     for (let i = 1; i <= b.length; i++) {
 84 |       for (let j = 1; j <= a.length; j++) {
 85 |         if (b.charAt(i - 1) === a.charAt(j - 1)) {
 86 |           matrix[i][j] = matrix[i - 1][j - 1];
 87 |         } else {
 88 |           matrix[i][j] = Math.min(
 89 |             matrix[i - 1][j - 1] + 1, // substitution
 90 |             matrix[i][j - 1] + 1,     // insertion
 91 |             matrix[i - 1][j] + 1      // deletion
 92 |           );
 93 |         }
 94 |       }
 95 |     }
 96 | 
 97 |     return matrix[b.length][a.length];
 98 |   }
 99 | 
100 |   private tokenize(name: string): string[] {
101 |     return name.toLowerCase().split(/[_\s]+/);
102 |   }
103 | 
104 |   private calculateSimilarityScore(searchTokens: string[], targetTokens: string[]): number {
105 |     // First try exact token matching with position awareness
106 |     const searchStr = searchTokens.join('_');
107 |     const targetStr = targetTokens.join('_');
108 |     
109 |     // Perfect match
110 |     if (searchStr === targetStr) {
111 |       return 1.0;
112 |     }
113 |     
114 |     // Check if tokens are the same but in different order
115 |     const searchSet = new Set(searchTokens);
116 |     const targetSet = new Set(targetTokens);
117 |     if (searchSet.size === targetSet.size && 
118 |         [...searchSet].every(token => targetSet.has(token))) {
119 |       return 0.9;
120 |     }
121 | 
122 |     // Calculate token-by-token similarity
123 |     let score = 0;
124 |     const usedTargetTokens = new Set<number>();
125 |     let matchedTokens = 0;
126 | 
127 |     for (const searchToken of searchTokens) {
128 |       let bestTokenScore = 0;
129 |       let bestTokenIndex = -1;
130 | 
131 |       targetTokens.forEach((targetToken, index) => {
132 |         if (usedTargetTokens.has(index)) return;
133 | 
134 |         // Exact match gets highest score
135 |         if (searchToken === targetToken) {
136 |           const positionPenalty = Math.abs(searchTokens.indexOf(searchToken) - index) * 0.1;
137 |           const tokenScore = Math.max(0.8, 1.0 - positionPenalty);
138 |           if (tokenScore > bestTokenScore) {
139 |             bestTokenScore = tokenScore;
140 |             bestTokenIndex = index;
141 |           }
142 |           return;
143 |         }
144 | 
145 |         // Substring match gets good score
146 |         if (targetToken.includes(searchToken) || searchToken.includes(targetToken)) {
147 |           const tokenScore = 0.7;
148 |           if (tokenScore > bestTokenScore) {
149 |             bestTokenScore = tokenScore;
150 |             bestTokenIndex = index;
151 |           }
152 |           return;
153 |         }
154 | 
155 |         // Levenshtein distance for fuzzy matching
156 |         const distance = this.calculateLevenshteinDistance(searchToken, targetToken);
157 |         const maxLength = Math.max(searchToken.length, targetToken.length);
158 |         const tokenScore = 1 - (distance / maxLength);
159 |         
160 |         if (tokenScore > 0.6 && tokenScore > bestTokenScore) {
161 |           bestTokenScore = tokenScore;
162 |           bestTokenIndex = index;
163 |         }
164 |       });
165 | 
166 |       if (bestTokenIndex !== -1) {
167 |         score += bestTokenScore;
168 |         usedTargetTokens.add(bestTokenIndex);
169 |         matchedTokens++;
170 |       }
171 |     }
172 | 
173 |     // Penalize if not all tokens were matched
174 |     const matchRatio = matchedTokens / searchTokens.length;
175 |     const finalScore = (score / searchTokens.length) * matchRatio;
176 | 
177 |     // Additional penalty for length mismatch
178 |     const lengthPenalty = Math.abs(searchTokens.length - targetTokens.length) * 0.1;
179 |     return Math.max(0, finalScore - lengthPenalty);
180 |   }
181 | 
182 |   private isCommonTypo(a: string, b: string): boolean {
183 |     const commonTypos: { [key: string]: string[] } = {
184 |       'label': ['lable', 'labl', 'lbl'],
185 |       'email': ['emil', 'mail', 'emal'],
186 |       'calendar': ['calender', 'calander', 'caldr'],
187 |       'workspace': ['workspce', 'wrkspace', 'wrkspc'],
188 |       'create': ['creat', 'crete', 'craete'],
189 |       'message': ['mesage', 'msg', 'messge'],
190 |       'draft': ['draf', 'drft', 'darft']
191 |     };
192 | 
193 |     // Check both directions (a->b and b->a)
194 |     for (const [word, typos] of Object.entries(commonTypos)) {
195 |       if ((a === word && typos.includes(b)) || (b === word && typos.includes(a))) {
196 |         return true;
197 |       }
198 |     }
199 |     return false;
200 |   }
201 | 
202 |   findSimilarTools(name: string, maxSuggestions: number = 3): ToolMetadata[] {
203 |     const searchTokens = this.tokenize(name);
204 |     const matches: Array<{ tool: ToolMetadata; score: number }> = [];
205 | 
206 |     for (const tool of this.getAllTools()) {
207 |       let bestScore = 0;
208 | 
209 |       // Check main tool name
210 |       const nameTokens = this.tokenize(tool.name);
211 |       bestScore = this.calculateSimilarityScore(searchTokens, nameTokens);
212 | 
213 |       // Check for common typos in each token
214 |       const hasCommonTypo = searchTokens.some(searchToken =>
215 |         nameTokens.some(nameToken => this.isCommonTypo(searchToken, nameToken))
216 |       );
217 |       if (hasCommonTypo) {
218 |         bestScore = Math.max(bestScore, 0.8); // Boost score for common typos
219 |       }
220 | 
221 |       // Check aliases
222 |       if (tool.aliases) {
223 |         for (const alias of tool.aliases) {
224 |           const aliasTokens = this.tokenize(alias);
225 |           const aliasScore = this.calculateSimilarityScore(searchTokens, aliasTokens);
226 |           
227 |           // Check for common typos in aliases too
228 |           if (searchTokens.some(searchToken =>
229 |               aliasTokens.some(aliasToken => this.isCommonTypo(searchToken, aliasToken)))) {
230 |             bestScore = Math.max(bestScore, 0.8);
231 |           }
232 |           
233 |           bestScore = Math.max(bestScore, aliasScore);
234 |         }
235 |       }
236 | 
237 |       // More lenient threshold (0.4 instead of 0.5) and include common typos
238 |       if (bestScore > 0.4 || hasCommonTypo) {
239 |         matches.push({ tool, score: bestScore });
240 |       }
241 |     }
242 | 
243 |     // Sort by score (highest first) and return top matches
244 |     return matches
245 |       .sort((a, b) => b.score - a.score)
246 |       .slice(0, maxSuggestions)
247 |       .map(m => m.tool);
248 |   }
249 | 
250 |   formatErrorWithSuggestions(invalidToolName: string): string {
251 |     const similarTools = this.findSimilarTools(invalidToolName);
252 |     const categories = this.getCategories();
253 | 
254 |     let message = `Tool '${invalidToolName}' not found.\n\n`;
255 | 
256 |     if (similarTools.length > 0) {
257 |       message += 'Did you mean:\n';
258 |       for (const tool of similarTools) {
259 |         message += `- ${tool.name} (${tool.category})\n`;
260 |         if (tool.aliases && tool.aliases.length > 0) {
261 |           message += `  Aliases: ${tool.aliases.join(', ')}\n`;
262 |         }
263 |       }
264 |       message += '\n';
265 |     }
266 | 
267 |     message += 'Available categories:\n';
268 |     for (const category of categories) {
269 |       const toolNames = category.tools.map(t => t.name.replace('workspace_', '')).join(', ');
270 |       message += `- ${category.name}: ${toolNames}\n`;
271 |     }
272 | 
273 |     return message;
274 |   }
275 | 
276 |   // Helper method to get all available tool names including aliases
277 |   getAllToolNames(): string[] {
278 |     const names: string[] = [];
279 |     for (const tool of this.tools.values()) {
280 |       names.push(tool.name);
281 |       if (tool.aliases) {
282 |         names.push(...tool.aliases);
283 |       }
284 |     }
285 |     return names;
286 |   }
287 | }
288 | 
```
--------------------------------------------------------------------------------
/src/modules/gmail/__tests__/label.test.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { GmailService } from '../services/base.js';
  2 | import { gmail_v1 } from 'googleapis';
  3 | import { Label } from '../types.js';
  4 | import { getAccountManager } from '../../../modules/accounts/index.js';
  5 | import { AccountManager } from '../../../modules/accounts/manager.js';
  6 | import logger from '../../../utils/logger.js';
  7 | 
  8 | jest.mock('../../../modules/accounts/index.js');
  9 | jest.mock('../../../modules/accounts/manager.js');
 10 | jest.mock('../../../utils/logger.js', () => ({
 11 |   default: {
 12 |     error: jest.fn(),
 13 |     warn: jest.fn(),
 14 |     info: jest.fn(),
 15 |     debug: jest.fn()
 16 |   }
 17 | }));
 18 | 
 19 | describe('Gmail Label Service', () => {
 20 |   let gmailService: GmailService;
 21 |   let mockClient: any;
 22 |   const testEmail = '[email protected]';
 23 | 
 24 |   beforeAll(() => {
 25 |     // Mock getAccountManager at module level
 26 |     (getAccountManager as jest.Mock).mockReturnValue({
 27 |       validateToken: jest.fn().mockResolvedValue({ valid: true, token: {} }),
 28 |       getAuthClient: jest.fn().mockResolvedValue({})
 29 |     });
 30 |   });
 31 | 
 32 |   beforeEach(async () => {
 33 |     
 34 |     // Create a fresh instance for each test
 35 |     gmailService = new GmailService();
 36 | 
 37 |     // Create mock client
 38 |     mockClient = {
 39 |       users: {
 40 |         labels: {
 41 |           list: jest.fn(),
 42 |           create: jest.fn(),
 43 |           patch: jest.fn(),
 44 |           delete: jest.fn()
 45 |         },
 46 |         messages: {
 47 |           modify: jest.fn()
 48 |         }
 49 |       }
 50 |     };
 51 | 
 52 |     // Mock the Gmail client methods at service level
 53 |     (gmailService as any).getGmailClient = jest.fn().mockResolvedValue(mockClient);
 54 | 
 55 |     // Initialize the service and update label service client
 56 |     await gmailService.initialize();
 57 |     (gmailService as any).labelService.updateClient(mockClient);
 58 |   });
 59 | 
 60 |   describe('manageLabel - read', () => {
 61 |     it('should fetch all labels', async () => {
 62 |       // Simple mock response
 63 |       const mockResponse = {
 64 |         data: {
 65 |           labels: [
 66 |             {
 67 |               id: 'label1',
 68 |               name: 'Test Label',
 69 |               type: 'user',
 70 |               messageListVisibility: 'show',
 71 |               labelListVisibility: 'labelShow'
 72 |             }
 73 |           ]
 74 |         }
 75 |       };
 76 | 
 77 |       // Set up mock
 78 |       (mockClient.users.labels.list as jest.Mock).mockResolvedValue(mockResponse);
 79 | 
 80 |       const result = await gmailService.manageLabel({
 81 |         action: 'read',
 82 |         email: testEmail
 83 |       });
 84 | 
 85 |       // Simple assertions
 86 |       expect((result as any).labels).toHaveLength(1);
 87 |       expect((result as any).labels[0].id).toBe('label1');
 88 |       expect((result as any).labels[0].name).toBe('Test Label');
 89 |       expect(mockClient.users.labels.list).toHaveBeenCalledWith({
 90 |         userId: testEmail
 91 |       });
 92 |     });
 93 | 
 94 |     it('should handle empty labels response', async () => {
 95 |       // Simple mock for empty response
 96 |       (mockClient.users.labels.list as jest.Mock).mockResolvedValue({
 97 |         data: { labels: [] }
 98 |       });
 99 | 
100 |       const result = await gmailService.manageLabel({
101 |         action: 'read',
102 |         email: testEmail
103 |       });
104 |       expect((result as any).labels).toHaveLength(0);
105 |     });
106 | 
107 |     it('should handle errors when fetching labels', async () => {
108 |       // Simple error mock
109 |       (mockClient.users.labels.list as jest.Mock).mockRejectedValue(new Error('API Error'));
110 | 
111 |       await expect(gmailService.manageLabel({
112 |         action: 'read',
113 |         email: testEmail
114 |       }))
115 |         .rejects
116 |         .toThrow('Failed to fetch labels');
117 |     });
118 |   });
119 | 
120 |   describe('manageLabel - create', () => {
121 |     it('should create a new label', async () => {
122 |       // Simple mock response
123 |       const mockResponse = {
124 |         data: {
125 |           id: 'label1',
126 |           name: 'Test Label',
127 |           type: 'user',
128 |           messageListVisibility: 'show',
129 |           labelListVisibility: 'labelShow'
130 |         }
131 |       };
132 | 
133 |       // Set up mock
134 |       (mockClient.users.labels.create as jest.Mock).mockResolvedValue(mockResponse);
135 | 
136 |       const result = await gmailService.manageLabel({
137 |         action: 'create',
138 |         email: testEmail,
139 |         data: {
140 |           name: 'Test Label'
141 |         }
142 |       });
143 | 
144 |       // Simple assertions
145 |       expect((result as Label).id).toBe('label1');
146 |       expect((result as Label).name).toBe('Test Label');
147 |       expect(mockClient.users.labels.create).toHaveBeenCalledWith({
148 |         userId: testEmail,
149 |         requestBody: expect.objectContaining({
150 |           name: 'Test Label'
151 |         })
152 |       });
153 |     });
154 | 
155 |     it('should handle errors when creating a label', async () => {
156 |       // Simple error mock
157 |       (mockClient.users.labels.create as jest.Mock).mockRejectedValue(new Error('API Error'));
158 | 
159 |       await expect(gmailService.manageLabel({
160 |         action: 'create',
161 |         email: testEmail,
162 |         data: {
163 |           name: 'Test Label'
164 |         }
165 |       })).rejects.toThrow('Failed to create label');
166 |     });
167 |   });
168 | 
169 |   describe('manageLabel - update', () => {
170 |     it('should update an existing label', async () => {
171 |       // Simple mock response
172 |       const mockResponse = {
173 |         data: {
174 |           id: 'label1',
175 |           name: 'Updated Label',
176 |           type: 'user',
177 |           messageListVisibility: 'show',
178 |           labelListVisibility: 'labelShow'
179 |         }
180 |       };
181 | 
182 |       // Set up mock
183 |       (mockClient.users.labels.patch as jest.Mock).mockResolvedValue(mockResponse);
184 | 
185 |       const result = await gmailService.manageLabel({
186 |         action: 'update',
187 |         email: testEmail,
188 |         labelId: 'label1',
189 |         data: {
190 |           name: 'Updated Label'
191 |         }
192 |       });
193 | 
194 |       // Simple assertions
195 |       expect((result as Label).id).toBe('label1');
196 |       expect((result as Label).name).toBe('Updated Label');
197 |       expect(mockClient.users.labels.patch).toHaveBeenCalledWith({
198 |         userId: testEmail,
199 |         id: 'label1',
200 |         requestBody: expect.objectContaining({
201 |           name: 'Updated Label'
202 |         })
203 |       });
204 |     });
205 | 
206 |     it('should handle errors when updating a label', async () => {
207 |       // Simple error mock
208 |       (mockClient.users.labels.patch as jest.Mock).mockRejectedValue(new Error('API Error'));
209 | 
210 |       await expect(gmailService.manageLabel({
211 |         action: 'update',
212 |         email: testEmail,
213 |         labelId: 'label1',
214 |         data: {
215 |           name: 'Updated Label'
216 |         }
217 |       })).rejects.toThrow('Failed to update label');
218 |     });
219 |   });
220 | 
221 |   describe('manageLabel - delete', () => {
222 |     it('should delete a label', async () => {
223 |       // Simple mock response
224 |       (mockClient.users.labels.delete as jest.Mock).mockResolvedValue({});
225 | 
226 |       // Execute and verify
227 |       await gmailService.manageLabel({
228 |         action: 'delete',
229 |         email: testEmail,
230 |         labelId: 'label1'
231 |       });
232 | 
233 |       // Simple assertions
234 |       expect(mockClient.users.labels.delete).toHaveBeenCalledWith({
235 |         userId: testEmail,
236 |         id: 'label1'
237 |       });
238 |     });
239 | 
240 |     it('should handle errors when deleting a label', async () => {
241 |       // Simple error mock
242 |       (mockClient.users.labels.delete as jest.Mock).mockRejectedValue(new Error('API Error'));
243 | 
244 |       await expect(gmailService.manageLabel({
245 |         action: 'delete',
246 |         email: testEmail,
247 |         labelId: 'label1'
248 |       })).rejects.toThrow('Failed to delete label');
249 |     });
250 |   });
251 | 
252 |   describe('manageLabelAssignment', () => {
253 |     it('should add labels to a message', async () => {
254 |       // Simple mock response
255 |       (mockClient.users.messages.modify as jest.Mock).mockResolvedValue({});
256 | 
257 |       // Execute and verify
258 |       await gmailService.manageLabelAssignment({
259 |         action: 'add',
260 |         email: testEmail,
261 |         messageId: 'msg1',
262 |         labelIds: ['label1']
263 |       });
264 | 
265 |       // Simple assertions
266 |       expect(mockClient.users.messages.modify).toHaveBeenCalledWith({
267 |         userId: testEmail,
268 |         id: 'msg1',
269 |         requestBody: {
270 |           addLabelIds: ['label1'],
271 |           removeLabelIds: []
272 |         }
273 |       });
274 |     });
275 | 
276 |     it('should remove labels from a message', async () => {
277 |       // Simple mock response
278 |       (mockClient.users.messages.modify as jest.Mock).mockResolvedValue({});
279 | 
280 |       // Execute and verify
281 |       await gmailService.manageLabelAssignment({
282 |         action: 'remove',
283 |         email: testEmail,
284 |         messageId: 'msg1',
285 |         labelIds: ['label2']
286 |       });
287 | 
288 |       // Simple assertions
289 |       expect(mockClient.users.messages.modify).toHaveBeenCalledWith({
290 |         userId: testEmail,
291 |         id: 'msg1',
292 |         requestBody: {
293 |           addLabelIds: [],
294 |           removeLabelIds: ['label2']
295 |         }
296 |       });
297 |     });
298 | 
299 |     it('should handle errors when modifying message labels', async () => {
300 |       // Simple error mock
301 |       (mockClient.users.messages.modify as jest.Mock).mockRejectedValue(new Error('API Error'));
302 | 
303 |       await expect(gmailService.manageLabelAssignment({
304 |         action: 'add',
305 |         email: testEmail,
306 |         messageId: 'msg1',
307 |         labelIds: ['label1']
308 |       })).rejects.toThrow('Failed to modify message labels');
309 |     });
310 |   });
311 | });
312 | 
```