#
tokens: 32888/50000 4/127 files (page 4/4)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 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/tools/gmail-handlers.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { getGmailService } from '../modules/gmail/index.js';
  2 | import { validateEmail } from '../utils/account.js';
  3 | import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
  4 | import { SendEmailParams } from '../modules/gmail/types.js';
  5 | import {
  6 |   ManageLabelParams,
  7 |   ManageLabelAssignmentParams,
  8 |   ManageLabelFilterParams
  9 | } from '../modules/gmail/services/label.js';
 10 | import { getAccountManager } from '../modules/accounts/index.js';
 11 | import { AttachmentService } from '../modules/attachments/service.js';
 12 | import { ATTACHMENT_FOLDERS } from '../modules/attachments/types.js';
 13 | 
 14 | // Singleton instances
 15 | let gmailService: ReturnType<typeof getGmailService>;
 16 | let accountManager: ReturnType<typeof getAccountManager>;
 17 | let attachmentService: AttachmentService;
 18 | 
 19 | /**
 20 |  * Initialize required services
 21 |  */
 22 | async function initializeServices() {
 23 |   if (!gmailService) {
 24 |     gmailService = getGmailService();
 25 |     await gmailService.initialize();
 26 |   }
 27 |   
 28 |   if (!accountManager) {
 29 |     accountManager = getAccountManager();
 30 |   }
 31 | 
 32 |   if (!attachmentService) {
 33 |     attachmentService = AttachmentService.getInstance();
 34 |   }
 35 | }
 36 | 
 37 | import { 
 38 |   GmailAttachment, 
 39 |   OutgoingGmailAttachment,
 40 |   IncomingGmailAttachment
 41 | } from '../modules/gmail/types.js';
 42 | import { ManageAttachmentParams } from './types.js';
 43 | 
 44 | interface SearchEmailsParams {
 45 |   email: string;
 46 |   search?: {
 47 |     from?: string | string[];
 48 |     to?: string | string[];
 49 |     subject?: string;
 50 |     after?: string;
 51 |     before?: string;
 52 |     hasAttachment?: boolean;
 53 |     labels?: string[];
 54 |     excludeLabels?: string[];
 55 |     includeSpam?: boolean;
 56 |     isUnread?: boolean;
 57 |   };
 58 |   options?: {
 59 |     maxResults?: number;
 60 |     pageToken?: string;
 61 |     format?: 'full' | 'metadata' | 'minimal';
 62 |     includeHeaders?: boolean;
 63 |     threadedView?: boolean;
 64 |     sortOrder?: 'asc' | 'desc';
 65 |   };
 66 |   messageIds?: string[];
 67 | }
 68 | 
 69 | interface SendEmailRequestParams {
 70 |   email: string;
 71 |   to: string[];
 72 |   subject: string;
 73 |   body: string;
 74 |   cc?: string[];
 75 |   bcc?: string[];
 76 |   attachments?: OutgoingGmailAttachment[];
 77 | }
 78 | 
 79 | interface ManageDraftParams {
 80 |   email: string;
 81 |   action: 'create' | 'read' | 'update' | 'delete' | 'send';
 82 |   draftId?: string;
 83 |   data?: {
 84 |     to: string[];
 85 |     subject: string;
 86 |     body: string;
 87 |     cc?: string[];
 88 |     bcc?: string[];
 89 |     attachments?: OutgoingGmailAttachment[];
 90 |   };
 91 | }
 92 | 
 93 | export async function handleSearchWorkspaceEmails(params: SearchEmailsParams) {
 94 |   await initializeServices();
 95 |   const { email, search = {}, options = {}, messageIds } = params;
 96 | 
 97 |   if (!email) {
 98 |     throw new McpError(
 99 |       ErrorCode.InvalidParams,
100 |       'Email address is required'
101 |     );
102 |   }
103 | 
104 |   validateEmail(email);
105 | 
106 |   return accountManager.withTokenRenewal(email, async () => {
107 |     try {
108 |       return await gmailService.getEmails({ email, search, options, messageIds });
109 |     } catch (error) {
110 |       throw new McpError(
111 |         ErrorCode.InternalError,
112 |         `Failed to search emails: ${error instanceof Error ? error.message : 'Unknown error'}`
113 |       );
114 |     }
115 |   });
116 | }
117 | 
118 | export async function handleSendWorkspaceEmail(params: SendEmailRequestParams) {
119 |   await initializeServices();
120 |   const { email, to, subject, body, cc, bcc, attachments } = params;
121 | 
122 |   if (!email) {
123 |     throw new McpError(
124 |       ErrorCode.InvalidParams,
125 |       'Sender email address is required'
126 |     );
127 |   }
128 | 
129 |   if (!to || !Array.isArray(to) || to.length === 0) {
130 |     throw new McpError(
131 |       ErrorCode.InvalidParams,
132 |       'At least one recipient email address is required'
133 |     );
134 |   }
135 | 
136 |   if (!subject) {
137 |     throw new McpError(
138 |       ErrorCode.InvalidParams,
139 |       'Email subject is required'
140 |     );
141 |   }
142 | 
143 |   if (!body) {
144 |     throw new McpError(
145 |       ErrorCode.InvalidParams,
146 |       'Email body is required'
147 |     );
148 |   }
149 | 
150 |   validateEmail(email);
151 |   to.forEach(validateEmail);
152 |   if (cc) cc.forEach(validateEmail);
153 |   if (bcc) bcc.forEach(validateEmail);
154 | 
155 |   return accountManager.withTokenRenewal(email, async () => {
156 |     try {
157 |       const emailParams: SendEmailParams = {
158 |         email,
159 |         to,
160 |         subject,
161 |         body,
162 |         cc,
163 |         bcc,
164 |         attachments: attachments?.map(attachment => {
165 |           if (!attachment.content) {
166 |             throw new McpError(
167 |               ErrorCode.InvalidParams,
168 |               `Attachment content is required for file: ${attachment.name}`
169 |             );
170 |           }
171 |           return {
172 |             id: attachment.id,
173 |             name: attachment.name,
174 |             mimeType: attachment.mimeType,
175 |             size: attachment.size,
176 |             content: attachment.content
177 |           } as OutgoingGmailAttachment;
178 |         })
179 |       };
180 | 
181 |       return await gmailService.sendEmail(emailParams);
182 |     } catch (error) {
183 |       throw new McpError(
184 |         ErrorCode.InternalError,
185 |         `Failed to send email: ${error instanceof Error ? error.message : 'Unknown error'}`
186 |       );
187 |     }
188 |   });
189 | }
190 | 
191 | export async function handleGetWorkspaceGmailSettings(params: { email: string }) {
192 |   await initializeServices();
193 |   const { email } = params;
194 | 
195 |   if (!email) {
196 |     throw new McpError(
197 |       ErrorCode.InvalidParams,
198 |       'Email address is required'
199 |     );
200 |   }
201 | 
202 |   validateEmail(email);
203 | 
204 |   return accountManager.withTokenRenewal(email, async () => {
205 |     try {
206 |       return await gmailService.getWorkspaceGmailSettings({ email });
207 |     } catch (error) {
208 |       throw new McpError(
209 |         ErrorCode.InternalError,
210 |         `Failed to get Gmail settings: ${error instanceof Error ? error.message : 'Unknown error'}`
211 |       );
212 |     }
213 |   });
214 | }
215 | 
216 | export async function handleManageWorkspaceDraft(params: ManageDraftParams) {
217 |   await initializeServices();
218 |   const { email, action, draftId, data } = params;
219 | 
220 |   if (!email) {
221 |     throw new McpError(
222 |       ErrorCode.InvalidParams,
223 |       'Email address is required'
224 |     );
225 |   }
226 | 
227 |   if (!action) {
228 |     throw new McpError(
229 |       ErrorCode.InvalidParams,
230 |       'Action is required'
231 |     );
232 |   }
233 | 
234 |   validateEmail(email);
235 | 
236 |   return accountManager.withTokenRenewal(email, async () => {
237 |     try {
238 |       switch (action) {
239 |         case 'create':
240 |           if (!data) {
241 |             throw new McpError(
242 |               ErrorCode.InvalidParams,
243 |               'Draft data is required for create action'
244 |             );
245 |           }
246 |           return await gmailService.manageDraft({
247 |             email,
248 |             action: 'create',
249 |             data: {
250 |               ...data,
251 |               attachments: data.attachments?.map(attachment => {
252 |                 if (!attachment.content) {
253 |                   throw new McpError(
254 |                     ErrorCode.InvalidParams,
255 |                     `Attachment content is required for file: ${attachment.name}`
256 |                   );
257 |                 }
258 |                 return {
259 |                   id: attachment.id,
260 |                   name: attachment.name,
261 |                   mimeType: attachment.mimeType,
262 |                   size: attachment.size,
263 |                   content: attachment.content
264 |                 } as OutgoingGmailAttachment;
265 |               })
266 |             }
267 |           });
268 | 
269 |         case 'read':
270 |           return await gmailService.manageDraft({
271 |             email,
272 |             action: 'read',
273 |             draftId
274 |           });
275 | 
276 |         case 'update':
277 |           if (!draftId || !data) {
278 |             throw new McpError(
279 |               ErrorCode.InvalidParams,
280 |               'Draft ID and data are required for update action'
281 |             );
282 |           }
283 |           return await gmailService.manageDraft({
284 |             email,
285 |             action: 'update',
286 |             draftId,
287 |             data: {
288 |               ...data,
289 |               attachments: data.attachments?.map(attachment => {
290 |                 if (!attachment.content) {
291 |                   throw new McpError(
292 |                     ErrorCode.InvalidParams,
293 |                     `Attachment content is required for file: ${attachment.name}`
294 |                   );
295 |                 }
296 |                 return {
297 |                   id: attachment.id,
298 |                   name: attachment.name,
299 |                   mimeType: attachment.mimeType,
300 |                   size: attachment.size,
301 |                   content: attachment.content
302 |                 } as OutgoingGmailAttachment;
303 |               })
304 |             }
305 |           });
306 | 
307 |         case 'delete':
308 |           if (!draftId) {
309 |             throw new McpError(
310 |               ErrorCode.InvalidParams,
311 |               'Draft ID is required for delete action'
312 |             );
313 |           }
314 |           return await gmailService.manageDraft({
315 |             email,
316 |             action: 'delete',
317 |             draftId
318 |           });
319 | 
320 |         case 'send':
321 |           if (!draftId) {
322 |             throw new McpError(
323 |               ErrorCode.InvalidParams,
324 |               'Draft ID is required for send action'
325 |             );
326 |           }
327 |           return await gmailService.manageDraft({
328 |             email,
329 |             action: 'send',
330 |             draftId
331 |           });
332 | 
333 |         default:
334 |           throw new McpError(
335 |             ErrorCode.InvalidParams,
336 |             'Invalid action. Supported actions are: create, read, update, delete, send'
337 |           );
338 |       }
339 |     } catch (error) {
340 |       throw new McpError(
341 |         ErrorCode.InternalError,
342 |         `Failed to manage draft: ${error instanceof Error ? error.message : 'Unknown error'}`
343 |       );
344 |     }
345 |   });
346 | }
347 | 
348 | export async function handleManageWorkspaceLabel(params: ManageLabelParams) {
349 |   await initializeServices();
350 |   const { email } = params;
351 | 
352 |   if (!email) {
353 |     throw new McpError(
354 |       ErrorCode.InvalidParams,
355 |       'Email address is required'
356 |     );
357 |   }
358 | 
359 |   validateEmail(email);
360 | 
361 |   return accountManager.withTokenRenewal(email, async () => {
362 |     try {
363 |       return await gmailService.manageLabel(params);
364 |     } catch (error) {
365 |       throw new McpError(
366 |         ErrorCode.InternalError,
367 |         `Failed to manage label: ${error instanceof Error ? error.message : 'Unknown error'}`
368 |       );
369 |     }
370 |   });
371 | }
372 | 
373 | export async function handleManageWorkspaceLabelAssignment(params: ManageLabelAssignmentParams) {
374 |   await initializeServices();
375 |   const { email } = params;
376 | 
377 |   if (!email) {
378 |     throw new McpError(
379 |       ErrorCode.InvalidParams,
380 |       'Email address is required'
381 |     );
382 |   }
383 | 
384 |   validateEmail(email);
385 | 
386 |   return accountManager.withTokenRenewal(email, async () => {
387 |     try {
388 |       await gmailService.manageLabelAssignment(params);
389 |       return {
390 |         content: [{
391 |           type: 'text',
392 |           text: JSON.stringify({ success: true })
393 |         }]
394 |       };
395 |     } catch (error) {
396 |       throw new McpError(
397 |         ErrorCode.InternalError,
398 |         `Failed to manage label assignment: ${error instanceof Error ? error.message : 'Unknown error'}`
399 |       );
400 |     }
401 |   });
402 | }
403 | 
404 | export async function handleManageWorkspaceAttachment(params: ManageAttachmentParams) {
405 |   await initializeServices();
406 |   const { email, action, source, messageId, filename, content } = params;
407 | 
408 |   // Validate all required parameters
409 |   if (!email || !action || !source || !messageId || !filename) {
410 |     throw new McpError(
411 |       ErrorCode.InvalidRequest,
412 |       'Invalid attachment management parameters. Required: email, action, source, messageId, filename'
413 |     );
414 |   }
415 | 
416 |   validateEmail(email);
417 | 
418 |   return accountManager.withTokenRenewal(email, async () => {
419 |     try {
420 |       // Get shared attachment service instance
421 |       if (!attachmentService) {
422 |         attachmentService = AttachmentService.getInstance();
423 |         await attachmentService.initialize(email);
424 |       }
425 | 
426 |       // Determine parent folder based on source
427 |       const parentFolder = source === 'email' ? 
428 |         ATTACHMENT_FOLDERS.EMAIL : 
429 |         ATTACHMENT_FOLDERS.CALENDAR;
430 | 
431 |       switch (action) {
432 |         case 'download': {
433 |           // First get the attachment data from Gmail
434 |           const gmailAttachment = await gmailService.getAttachment(email, messageId, filename);
435 |           
436 |           if (!gmailAttachment || !gmailAttachment.content) {
437 |             throw new McpError(
438 |               ErrorCode.InvalidRequest,
439 |               'Attachment not found or content missing'
440 |             );
441 |           }
442 | 
443 |           // Process and save the attachment locally
444 |           const result = await attachmentService.processAttachment(
445 |             email,
446 |             {
447 |               content: gmailAttachment.content,
448 |               metadata: {
449 |                 name: gmailAttachment.name || `attachment_${Date.now()}`,
450 |                 mimeType: gmailAttachment.mimeType || 'application/octet-stream',
451 |                 size: gmailAttachment.size || 0
452 |               }
453 |             },
454 |             parentFolder
455 |           );
456 | 
457 |           if (!result.success) {
458 |             throw new McpError(
459 |               ErrorCode.InternalError,
460 |               `Failed to save attachment: ${result.error}`
461 |             );
462 |           }
463 | 
464 |           return {
465 |             success: true,
466 |             attachment: result.attachment
467 |           };
468 |         }
469 | 
470 |         case 'upload': {
471 |           if (!content) {
472 |             throw new McpError(
473 |               ErrorCode.InvalidParams,
474 |               'File content is required for upload'
475 |             );
476 |           }
477 | 
478 |           // Process attachment
479 |           const result = await attachmentService.processAttachment(
480 |             email,
481 |             {
482 |               content,
483 |               metadata: {
484 |                 name: `attachment_${Date.now()}`, // Default name if not provided
485 |                 mimeType: 'application/octet-stream', // Default type if not provided
486 |               }
487 |             },
488 |             parentFolder
489 |           );
490 | 
491 |           if (!result.success) {
492 |             throw new McpError(
493 |               ErrorCode.InternalError,
494 |               `Failed to upload attachment: ${result.error}`
495 |             );
496 |           }
497 | 
498 |           return {
499 |             success: true,
500 |             attachment: result.attachment
501 |           };
502 |         }
503 | 
504 |         case 'delete': {
505 |           // Get the attachment metadata first to verify it exists
506 |           const gmailAttachment = await gmailService.getAttachment(email, messageId, filename);
507 |           
508 |           if (!gmailAttachment || !gmailAttachment.path) {
509 |             throw new McpError(
510 |               ErrorCode.InvalidRequest,
511 |               'Attachment not found or path missing'
512 |             );
513 |           }
514 | 
515 |           // Delete the attachment
516 |           const result = await attachmentService.deleteAttachment(
517 |             email,
518 |             gmailAttachment.id,
519 |             gmailAttachment.path
520 |           );
521 | 
522 |           if (!result.success) {
523 |             throw new McpError(
524 |               ErrorCode.InternalError,
525 |               `Failed to delete attachment: ${result.error}`
526 |             );
527 |           }
528 | 
529 |           return {
530 |             success: true,
531 |             attachment: result.attachment
532 |           };
533 |         }
534 | 
535 |         default:
536 |           throw new McpError(
537 |             ErrorCode.InvalidParams,
538 |             'Invalid action. Supported actions are: download, upload, delete'
539 |           );
540 |       }
541 |     } catch (error) {
542 |       if (error instanceof McpError) {
543 |         throw error;
544 |       }
545 |       throw new McpError(
546 |         ErrorCode.InternalError,
547 |         `Failed to manage attachment: ${error instanceof Error ? error.message : 'Unknown error'}`
548 |       );
549 |     }
550 |   });
551 | }
552 | 
553 | export async function handleManageWorkspaceLabelFilter(params: ManageLabelFilterParams) {
554 |   await initializeServices();
555 |   const { email } = params;
556 | 
557 |   if (!email) {
558 |     throw new McpError(
559 |       ErrorCode.InvalidParams,
560 |       'Email address is required'
561 |     );
562 |   }
563 | 
564 |   validateEmail(email);
565 | 
566 |   return accountManager.withTokenRenewal(email, async () => {
567 |     try {
568 |       return await gmailService.manageLabelFilter(params);
569 |     } catch (error) {
570 |       throw new McpError(
571 |         ErrorCode.InternalError,
572 |         `Failed to manage label filter: ${error instanceof Error ? error.message : 'Unknown error'}`
573 |       );
574 |     }
575 |   });
576 | }
577 | 
```

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

```typescript
  1 | import { google, calendar_v3 } from 'googleapis';
  2 | import { OAuth2Client } from 'google-auth-library';
  3 | import path from 'path';
  4 | import { getAccountManager } from '../accounts/index.js';
  5 | import {
  6 |   GetEventsParams,
  7 |   CreateEventParams,
  8 |   EventResponse,
  9 |   CreateEventResponse,
 10 |   CalendarError,
 11 |   CalendarModuleConfig,
 12 |   ManageEventParams,
 13 |   ManageEventResponse,
 14 |   CalendarAttachment
 15 | } from './types.js';
 16 | import { AttachmentService } from '../attachments/service.js';
 17 | import { DriveService } from '../drive/service.js';
 18 | import { ATTACHMENT_FOLDERS, AttachmentSource, AttachmentMetadata } from '../attachments/types.js';
 19 | 
 20 | type CalendarEvent = calendar_v3.Schema$Event;
 21 | type GoogleEventAttachment = calendar_v3.Schema$EventAttachment;
 22 | 
 23 | // Convert Google Calendar attachment to our format
 24 | function convertToCalendarAttachment(attachment: GoogleEventAttachment): CalendarAttachment {
 25 |   return {
 26 |     content: attachment.fileUrl || '', // Use fileUrl as content for now
 27 |     title: attachment.title || 'untitled',
 28 |     mimeType: attachment.mimeType || 'application/octet-stream',
 29 |     size: 0 // Size not available from Calendar API
 30 |   };
 31 | }
 32 | 
 33 | // Convert our attachment format to Google Calendar format
 34 | function convertToGoogleAttachment(attachment: CalendarAttachment): GoogleEventAttachment {
 35 |   return {
 36 |     title: attachment.title,
 37 |     mimeType: attachment.mimeType,
 38 |     fileUrl: attachment.content // Store content in fileUrl
 39 |   };
 40 | }
 41 | 
 42 | /**
 43 |  * Google Calendar Service Implementation
 44 |  */
 45 | export class CalendarService {
 46 |   private oauth2Client!: OAuth2Client;
 47 |   private attachmentService?: AttachmentService;
 48 |   private driveService?: DriveService;
 49 |   private initialized = false;
 50 |   private config?: CalendarModuleConfig;
 51 | 
 52 |   constructor(config?: CalendarModuleConfig) {
 53 |     this.config = config;
 54 |   }
 55 | 
 56 |   /**
 57 |    * Initialize the Calendar service and all dependencies
 58 |    */
 59 |   public async initialize(): Promise<void> {
 60 |     try {
 61 |       const accountManager = getAccountManager();
 62 |       this.oauth2Client = await accountManager.getAuthClient();
 63 |       this.driveService = new DriveService();
 64 |       await this.driveService.ensureInitialized();
 65 |       this.attachmentService = AttachmentService.getInstance({
 66 |         maxSizeBytes: this.config?.maxAttachmentSize,
 67 |         allowedMimeTypes: this.config?.allowedAttachmentTypes
 68 |       });
 69 |       this.initialized = true;
 70 |     } catch (error) {
 71 |       throw new CalendarError(
 72 |         'Failed to initialize Calendar service',
 73 |         'INIT_ERROR',
 74 |         error instanceof Error ? error.message : 'Unknown error'
 75 |       );
 76 |     }
 77 |   }
 78 | 
 79 |   /**
 80 |    * Ensure the Calendar service is initialized
 81 |    */
 82 |   public async ensureInitialized(): Promise<void> {
 83 |     if (!this.initialized) {
 84 |       await this.initialize();
 85 |     }
 86 |   }
 87 | 
 88 |   /**
 89 |    * Check if the service is initialized
 90 |    */
 91 |   private checkInitialized(): void {
 92 |     if (!this.initialized) {
 93 |       throw new CalendarError(
 94 |         'Calendar service not initialized',
 95 |         'INIT_ERROR',
 96 |         'Please ensure the service is initialized before use'
 97 |       );
 98 |     }
 99 |   }
100 | 
101 |   /**
102 |    * Get an authenticated Google Calendar API client
103 |    */
104 |   private async getCalendarClient(email: string) {
105 |     if (!this.initialized) {
106 |       await this.initialize();
107 |     }
108 |     const accountManager = getAccountManager();
109 |     try {
110 |       const tokenStatus = await accountManager.validateToken(email);
111 |       if (!tokenStatus.valid || !tokenStatus.token) {
112 |         throw new CalendarError(
113 |           'Calendar authentication required',
114 |           'AUTH_REQUIRED',
115 |           'Please authenticate to access calendar'
116 |         );
117 |       }
118 | 
119 |       this.oauth2Client.setCredentials(tokenStatus.token);
120 |       return google.calendar({ version: 'v3', auth: this.oauth2Client });
121 |     } catch (error) {
122 |       if (error instanceof CalendarError) {
123 |         throw error;
124 |       }
125 |       throw new CalendarError(
126 |         'Failed to initialize Calendar client',
127 |         'AUTH_ERROR',
128 |         'Please try again or contact support if the issue persists'
129 |       );
130 |     }
131 |   }
132 | 
133 |   /**
134 |    * Handle Calendar operations with automatic token refresh on 401/403
135 |    */
136 |   private async handleCalendarOperation<T>(email: string, operation: () => Promise<T>): Promise<T> {
137 |     try {
138 |       return await operation();
139 |     } catch (error: any) {
140 |       if (error.code === 401 || error.code === 403) {
141 |         const accountManager = getAccountManager();
142 |         const tokenStatus = await accountManager.validateToken(email);
143 |         if (tokenStatus.valid && tokenStatus.token) {
144 |           this.oauth2Client.setCredentials(tokenStatus.token);
145 |           return await operation();
146 |         }
147 |       }
148 |       throw error;
149 |     }
150 |   }
151 | 
152 |   /**
153 |    * Process event attachments directly like Gmail
154 |    */
155 |   private async processEventAttachments(
156 |     email: string,
157 |     attachments: GoogleEventAttachment[]
158 |   ): Promise<AttachmentMetadata[]> {
159 |     if (!this.attachmentService) {
160 |       throw new CalendarError(
161 |         'Calendar service not initialized',
162 |         'SERVICE_ERROR',
163 |         'Please ensure the service is initialized before processing attachments'
164 |       );
165 |     }
166 |     const processedAttachments: AttachmentMetadata[] = [];
167 | 
168 |     for (const googleAttachment of attachments) {
169 |       // Convert Google attachment to our format
170 |       const attachment = convertToCalendarAttachment(googleAttachment);
171 | 
172 |       const result = await this.attachmentService.processAttachment(
173 |         email,
174 |         {
175 |           content: attachment.content,
176 |           metadata: {
177 |             name: attachment.title,
178 |             mimeType: attachment.mimeType,
179 |             size: attachment.size
180 |           }
181 |         },
182 |         ATTACHMENT_FOLDERS.EVENT_FILES
183 |       );
184 | 
185 |       if (result.success && result.attachment) {
186 |         processedAttachments.push(result.attachment);
187 |       }
188 |     }
189 | 
190 |     return processedAttachments;
191 |   }
192 | 
193 |   /**
194 |    * Map Calendar event to EventResponse
195 |    */
196 |   private async mapEventResponse(email: string, event: CalendarEvent): Promise<EventResponse> {
197 |     // Process attachments if present
198 |     let attachments: { name: string }[] | undefined;
199 |     
200 |     if (event.attachments && event.attachments.length > 0) {
201 |       // First process and store full metadata
202 |       const processedAttachments = await this.processEventAttachments(email, event.attachments);
203 |       
204 |       // Then return simplified format for AI
205 |       attachments = processedAttachments.map(att => ({
206 |         name: att.name
207 |       }));
208 |     }
209 | 
210 |     return {
211 |       id: event.id!,
212 |       summary: event.summary || '',
213 |       description: event.description || undefined,
214 |       start: {
215 |         dateTime: event.start?.dateTime || event.start?.date || '',
216 |         timeZone: event.start?.timeZone || 'UTC'
217 |       },
218 |       end: {
219 |         dateTime: event.end?.dateTime || event.end?.date || '',
220 |         timeZone: event.end?.timeZone || 'UTC'
221 |       },
222 |       attendees: event.attendees?.map(attendee => ({
223 |         email: attendee.email!,
224 |         responseStatus: attendee.responseStatus || undefined
225 |       })),
226 |       organizer: event.organizer ? {
227 |         email: event.organizer.email!,
228 |         self: event.organizer.self || false
229 |       } : undefined,
230 |       attachments: attachments?.length ? attachments : undefined
231 |     };
232 |   }
233 | 
234 |   /**
235 |    * Retrieve calendar events with optional filtering
236 |    */
237 |   async getEvents({ email, query, maxResults = 10, timeMin, timeMax }: GetEventsParams): Promise<EventResponse[]> {
238 |     const calendar = await this.getCalendarClient(email);
239 | 
240 |     return this.handleCalendarOperation(email, async () => {
241 |       const params: calendar_v3.Params$Resource$Events$List = {
242 |         calendarId: 'primary',
243 |         maxResults,
244 |         singleEvents: true,
245 |         orderBy: 'startTime'
246 |       };
247 | 
248 |       if (query) {
249 |         params.q = query;
250 |       }
251 | 
252 |       if (timeMin) {
253 |         try {
254 |           const date = new Date(timeMin);
255 |           if (isNaN(date.getTime())) {
256 |             throw new Error('Invalid date');
257 |           }
258 |           params.timeMin = date.toISOString();
259 |         } catch (error) {
260 |           throw new CalendarError(
261 |             'Invalid date format',
262 |             'INVALID_DATE',
263 |             'Please provide dates in ISO format or YYYY-MM-DD format'
264 |           );
265 |         }
266 |       }
267 | 
268 |       if (timeMax) {
269 |         try {
270 |           const date = new Date(timeMax);
271 |           if (isNaN(date.getTime())) {
272 |             throw new Error('Invalid date');
273 |           }
274 |           params.timeMax = date.toISOString();
275 |         } catch (error) {
276 |           throw new CalendarError(
277 |             'Invalid date format',
278 |             'INVALID_DATE',
279 |             'Please provide dates in ISO format or YYYY-MM-DD format'
280 |           );
281 |         }
282 |       }
283 | 
284 |       const { data } = await calendar.events.list(params);
285 | 
286 |       if (!data.items || data.items.length === 0) {
287 |         return [];
288 |       }
289 | 
290 |       return Promise.all(data.items.map(event => this.mapEventResponse(email, event)));
291 |     });
292 |   }
293 | 
294 |   /**
295 |    * Retrieve a single calendar event by ID
296 |    */
297 |   async getEvent(email: string, eventId: string): Promise<EventResponse> {
298 |     const calendar = await this.getCalendarClient(email);
299 | 
300 |     return this.handleCalendarOperation(email, async () => {
301 |       const { data: event } = await calendar.events.get({
302 |         calendarId: 'primary',
303 |         eventId
304 |       });
305 | 
306 |       if (!event) {
307 |         throw new CalendarError(
308 |           'Event not found',
309 |           'NOT_FOUND',
310 |           `No event found with ID: ${eventId}`
311 |         );
312 |       }
313 | 
314 |       return this.mapEventResponse(email, event);
315 |     });
316 |   }
317 | 
318 |   /**
319 |    * Create a new calendar event
320 |    */
321 |   async createEvent({ email, summary, description, start, end, attendees, attachments = [] }: CreateEventParams): Promise<CreateEventResponse> {
322 |     const calendar = await this.getCalendarClient(email);
323 | 
324 |     return this.handleCalendarOperation(email, async () => {
325 |       // Process attachments first
326 |       const processedAttachments: GoogleEventAttachment[] = [];
327 |       for (const attachment of attachments) {
328 |         const source: AttachmentSource = {
329 |           content: attachment.content || '',
330 |           metadata: {
331 |             name: attachment.name,
332 |             mimeType: attachment.mimeType,
333 |             size: attachment.size
334 |           }
335 |         };
336 | 
337 |         if (!this.attachmentService) {
338 |           throw new CalendarError(
339 |             'Calendar service not initialized',
340 |             'SERVICE_ERROR',
341 |             'Please ensure the service is initialized before processing attachments'
342 |           );
343 |         }
344 |         const result = await this.attachmentService.processAttachment(
345 |           email,
346 |           source,
347 |           ATTACHMENT_FOLDERS.EVENT_FILES
348 |         );
349 | 
350 |         if (result.success && result.attachment) {
351 |           // Convert back to Google format
352 |           processedAttachments.push(convertToGoogleAttachment({
353 |             content: result.attachment.id, // Use ID as content
354 |             title: result.attachment.name,
355 |             mimeType: result.attachment.mimeType,
356 |             size: result.attachment.size
357 |           }));
358 |         }
359 |       }
360 | 
361 |       const eventData: calendar_v3.Schema$Event = {
362 |         summary,
363 |         description,
364 |         start,
365 |         end,
366 |         attendees: attendees?.map(attendee => ({ email: attendee.email })),
367 |         attachments: processedAttachments.length > 0 ? processedAttachments : undefined
368 |       };
369 | 
370 |       const { data: event } = await calendar.events.insert({
371 |         calendarId: 'primary',
372 |         requestBody: eventData,
373 |         sendUpdates: 'all'
374 |       });
375 | 
376 |       if (!event.id || !event.summary) {
377 |         throw new CalendarError(
378 |           'Failed to create event',
379 |           'CREATE_ERROR',
380 |           'Event creation response was incomplete'
381 |         );
382 |       }
383 | 
384 |       // Convert processed attachments to AttachmentMetadata format
385 |       const attachmentMetadata = processedAttachments.length > 0 ? 
386 |         processedAttachments.map(a => ({
387 |           id: a.fileId!,
388 |           name: a.title!,
389 |           mimeType: a.mimeType!,
390 |           size: 0, // Size not available from Calendar API
391 |           path: path.join(this.attachmentService!.getAttachmentPath(ATTACHMENT_FOLDERS.EVENT_FILES), `${a.fileId}_${a.title}`)
392 |         })) : 
393 |         undefined;
394 | 
395 |       return {
396 |         id: event.id,
397 |         summary: event.summary,
398 |         htmlLink: event.htmlLink || '',
399 |         attachments: attachmentMetadata
400 |       };
401 |     });
402 |   }
403 | 
404 |   /**
405 |    * Manage calendar event responses and updates
406 |    */
407 |   async manageEvent({ email, eventId, action, comment, newTimes }: ManageEventParams): Promise<ManageEventResponse> {
408 |     const calendar = await this.getCalendarClient(email);
409 | 
410 |     return this.handleCalendarOperation(email, async () => {
411 |       const event = await calendar.events.get({
412 |         calendarId: 'primary',
413 |         eventId
414 |       });
415 | 
416 |       if (!event.data) {
417 |         throw new CalendarError(
418 |           'Event not found',
419 |           'NOT_FOUND',
420 |           `No event found with ID: ${eventId}`
421 |         );
422 |       }
423 | 
424 |       switch (action) {
425 |         case 'accept':
426 |         case 'decline':
427 |         case 'tentative': {
428 |           const responseStatus = action === 'accept' ? 'accepted' : 
429 |                                action === 'decline' ? 'declined' : 
430 |                                'tentative';
431 | 
432 |           const { data: updatedEvent } = await calendar.events.patch({
433 |             calendarId: 'primary',
434 |             eventId,
435 |             sendUpdates: 'all',
436 |             requestBody: {
437 |               attendees: [
438 |                 {
439 |                   email,
440 |                   responseStatus
441 |                 }
442 |               ]
443 |             }
444 |           });
445 | 
446 |           return {
447 |             success: true,
448 |             eventId,
449 |             action,
450 |             status: 'completed',
451 |             htmlLink: updatedEvent.htmlLink || undefined
452 |           };
453 |         }
454 | 
455 |         case 'propose_new_time': {
456 |           if (!newTimes || newTimes.length === 0) {
457 |             throw new CalendarError(
458 |               'No proposed times provided',
459 |               'INVALID_REQUEST',
460 |               'Please provide at least one proposed time'
461 |             );
462 |           }
463 | 
464 |           const counterProposal = await calendar.events.insert({
465 |             calendarId: 'primary',
466 |             requestBody: {
467 |               summary: `Counter-proposal: ${event.data.summary}`,
468 |               description: `Counter-proposal for original event.\n\nComment: ${comment || 'No comment provided'}\n\nOriginal event: ${event.data.htmlLink}`,
469 |               start: newTimes[0].start,
470 |               end: newTimes[0].end,
471 |               attendees: event.data.attendees
472 |             }
473 |           });
474 | 
475 |           return {
476 |             success: true,
477 |             eventId,
478 |             action,
479 |             status: 'proposed',
480 |             htmlLink: counterProposal.data.htmlLink || undefined,
481 |             proposedTimes: newTimes.map(time => ({
482 |               start: { dateTime: time.start.dateTime, timeZone: time.start.timeZone || 'UTC' },
483 |               end: { dateTime: time.end.dateTime, timeZone: time.end.timeZone || 'UTC' }
484 |             }))
485 |           };
486 |         }
487 | 
488 |         case 'update_time': {
489 |           if (!newTimes || newTimes.length === 0) {
490 |             throw new CalendarError(
491 |               'No new time provided',
492 |               'INVALID_REQUEST',
493 |               'Please provide the new time for the event'
494 |             );
495 |           }
496 | 
497 |           const { data: updatedEvent } = await calendar.events.patch({
498 |             calendarId: 'primary',
499 |             eventId,
500 |             requestBody: {
501 |               start: newTimes[0].start,
502 |               end: newTimes[0].end
503 |             },
504 |             sendUpdates: 'all'
505 |           });
506 | 
507 |           return {
508 |             success: true,
509 |             eventId,
510 |             action,
511 |             status: 'updated',
512 |             htmlLink: updatedEvent.htmlLink || undefined
513 |           };
514 |         }
515 | 
516 |         default:
517 |           throw new CalendarError(
518 |             'Supported actions are: accept, decline, tentative, propose_new_time, update_time',
519 |             'INVALID_ACTION',
520 |             'Invalid action'
521 |           );
522 |       }
523 |     });
524 |   }
525 | 
526 |   /**
527 |    * Delete a calendar event
528 |    * 
529 |    * @param email User's email address
530 |    * @param eventId ID of the event to delete
531 |    * @param sendUpdates Whether to send update notifications
532 |    * @param deletionScope For recurring events, specifies which instances to delete
533 |    */
534 |   async deleteEvent(
535 |     email: string, 
536 |     eventId: string, 
537 |     sendUpdates: 'all' | 'externalOnly' | 'none' = 'all',
538 |     deletionScope?: 'entire_series' | 'this_and_following'
539 |   ): Promise<void> {
540 |     const calendar = await this.getCalendarClient(email);
541 | 
542 |     return this.handleCalendarOperation(email, async () => {
543 |       // If no deletion scope is specified or it's set to delete entire series,
544 |       // use the default behavior
545 |       if (!deletionScope || deletionScope === 'entire_series') {
546 |         await calendar.events.delete({
547 |           calendarId: 'primary',
548 |           eventId,
549 |           sendUpdates
550 |         });
551 |         return;
552 |       }
553 | 
554 |       // For 'this_and_following', we need to check if this is a recurring event
555 |       try {
556 |         const { data: event } = await calendar.events.get({
557 |           calendarId: 'primary',
558 |           eventId
559 |         });
560 |         
561 |         const isRecurring = !!event.recurringEventId || !!event.recurrence;
562 |         
563 |         if (!isRecurring) {
564 |           throw new CalendarError(
565 |             'Deletion scope can only be applied to recurring events',
566 |             'INVALID_REQUEST',
567 |             'The specified event is not recurring'
568 |           );
569 |         }
570 | 
571 |         // For 'this_and_following', we need to use a different approach
572 |         // Google Calendar API handles recurring events differently
573 |         
574 |         if (event.recurringEventId) {
575 |           // This is an instance of a recurring event
576 |           // We need to get the master event to update its recurrence rules
577 |           const { data: masterEvent } = await calendar.events.get({
578 |             calendarId: 'primary',
579 |             eventId: event.recurringEventId
580 |           });
581 |           
582 |           if (!masterEvent || !masterEvent.recurrence) {
583 |             throw new CalendarError(
584 |               'Failed to retrieve master event',
585 |               'NOT_FOUND',
586 |               'Could not find the master event for this recurring instance'
587 |             );
588 |           }
589 |           
590 |           // Get the instance date to use as the cutoff
591 |           const instanceDate = new Date(event.start?.dateTime || event.start?.date || '');
592 |           
593 |           // Format the date as YYYYMMDD for UNTIL parameter in RRULE
594 |           // Subtract one day to make it exclusive (end before this instance)
595 |           instanceDate.setDate(instanceDate.getDate() - 1);
596 |           const formattedDate = instanceDate.toISOString().slice(0, 10).replace(/-/g, '');
597 |           
598 |           // Update the recurrence rules to end before this instance
599 |           const updatedRules = masterEvent.recurrence.map(rule => {
600 |             if (rule.startsWith('RRULE:')) {
601 |               // If the rule already has an UNTIL parameter, we need to replace it
602 |               if (rule.includes('UNTIL=')) {
603 |                 return rule.replace(/UNTIL=\d+T\d+Z?/, `UNTIL=${formattedDate}`);
604 |               } else {
605 |                 // Otherwise, add the UNTIL parameter
606 |                 return `${rule};UNTIL=${formattedDate}`;
607 |               }
608 |             }
609 |             return rule;
610 |           });
611 |           
612 |           // Update the master event with the new recurrence rules
613 |           await calendar.events.patch({
614 |             calendarId: 'primary',
615 |             eventId: event.recurringEventId,
616 |             sendUpdates,
617 |             requestBody: {
618 |               recurrence: updatedRules
619 |             }
620 |           });
621 |           
622 |           // Now delete this specific instance
623 |           await calendar.events.delete({
624 |             calendarId: 'primary',
625 |             eventId,
626 |             sendUpdates
627 |           });
628 |         } else if (event.recurrence) {
629 |           // This is a master event with recurrence rules
630 |           // We need to update the recurrence rule to end before this instance
631 |           const eventDate = new Date(event.start?.dateTime || event.start?.date || '');
632 |           
633 |           // Format the date as YYYYMMDD for UNTIL parameter in RRULE
634 |           const formattedDate = eventDate.toISOString().slice(0, 10).replace(/-/g, '');
635 |           
636 |           // Get the existing recurrence rules
637 |           const recurrenceRules = event.recurrence || [];
638 |           
639 |           // Update the RRULE to include UNTIL parameter
640 |           const updatedRules = recurrenceRules.map(rule => {
641 |             if (rule.startsWith('RRULE:')) {
642 |               // If the rule already has an UNTIL parameter, we need to replace it
643 |               if (rule.includes('UNTIL=')) {
644 |                 return rule.replace(/UNTIL=\d+T\d+Z?/, `UNTIL=${formattedDate}`);
645 |               } else {
646 |                 // Otherwise, add the UNTIL parameter
647 |                 return `${rule};UNTIL=${formattedDate}`;
648 |               }
649 |             }
650 |             return rule;
651 |           });
652 |           
653 |           // Update the master event with the new recurrence rules
654 |           await calendar.events.patch({
655 |             calendarId: 'primary',
656 |             eventId,
657 |             sendUpdates,
658 |             requestBody: {
659 |               recurrence: updatedRules
660 |             }
661 |           });
662 |         }
663 |       } catch (error) {
664 |         if (error instanceof CalendarError) {
665 |           throw error;
666 |         }
667 |         
668 |         // If we can't get the event or another error occurs, fall back to simple delete
669 |         await calendar.events.delete({
670 |           calendarId: 'primary',
671 |           eventId,
672 |           sendUpdates
673 |         });
674 |       }
675 |     });
676 |   }
677 | }
678 | 
```

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

```typescript
  1 | import { gmail_v1 } from 'googleapis';
  2 | import {
  3 |   Label,
  4 |   CreateLabelParams,
  5 |   UpdateLabelParams,
  6 |   DeleteLabelParams,
  7 |   GetLabelsParams,
  8 |   GetLabelsResponse,
  9 |   ModifyMessageLabelsParams,
 10 |   GmailError,
 11 |   CreateLabelFilterParams,
 12 |   GetLabelFiltersParams,
 13 |   GetLabelFiltersResponse,
 14 |   UpdateLabelFilterParams,
 15 |   DeleteLabelFilterParams,
 16 |   LabelFilterCriteria,
 17 |   LabelFilterActions,
 18 |   LabelFilter
 19 | } from '../types.js';
 20 | import {
 21 |   isValidGmailLabelColor,
 22 |   getNearestGmailLabelColor,
 23 |   LABEL_ERROR_MESSAGES
 24 | } from '../constants.js';
 25 | 
 26 | export type LabelAction = 'create' | 'read' | 'update' | 'delete';
 27 | export type LabelAssignmentAction = 'add' | 'remove';
 28 | export type LabelFilterAction = 'create' | 'read' | 'update' | 'delete';
 29 | 
 30 | export interface ManageLabelParams {
 31 |   action: LabelAction;
 32 |   email: string;
 33 |   labelId?: string;
 34 |   data?: {
 35 |     name?: string;
 36 |     messageListVisibility?: 'show' | 'hide';
 37 |     labelListVisibility?: 'labelShow' | 'labelHide' | 'labelShowIfUnread';
 38 |     color?: {
 39 |       textColor: string;
 40 |       backgroundColor: string;
 41 |     };
 42 |   };
 43 | }
 44 | 
 45 | export interface ManageLabelAssignmentParams {
 46 |   action: LabelAssignmentAction;
 47 |   email: string;
 48 |   messageId: string;
 49 |   labelIds: string[];
 50 | }
 51 | 
 52 | export interface ManageLabelFilterParams {
 53 |   action: LabelFilterAction;
 54 |   email: string;
 55 |   filterId?: string;
 56 |   labelId?: string;
 57 |   data?: {
 58 |     criteria: LabelFilterCriteria;
 59 |     actions: LabelFilterActions;
 60 |   };
 61 | }
 62 | 
 63 | export class LabelService {
 64 |   private client: gmail_v1.Gmail | null = null;
 65 | 
 66 |   updateClient(client: gmail_v1.Gmail) {
 67 |     this.client = client;
 68 |   }
 69 | 
 70 |   private ensureClient() {
 71 |     if (!this.client) {
 72 |       throw new GmailError(
 73 |         'Gmail client not initialized',
 74 |         'CLIENT_ERROR',
 75 |         'Please ensure the service is initialized with a valid client'
 76 |       );
 77 |     }
 78 |   }
 79 | 
 80 |   async manageLabel(params: ManageLabelParams): Promise<Label | GetLabelsResponse | void> {
 81 |     this.ensureClient();
 82 | 
 83 |     switch (params.action) {
 84 |       case 'create':
 85 |         if (!params.data?.name) {
 86 |           throw new GmailError(
 87 |             'Label name is required for creation',
 88 |             'VALIDATION_ERROR',
 89 |             'Please provide a name for the label'
 90 |           );
 91 |         }
 92 |         return this.createLabel({
 93 |           email: params.email,
 94 |           name: params.data.name,
 95 |           messageListVisibility: params.data.messageListVisibility,
 96 |           labelListVisibility: params.data.labelListVisibility,
 97 |           color: params.data.color
 98 |         });
 99 | 
100 |       case 'read':
101 |         if (params.labelId) {
102 |           // Get specific label
103 |           const response = await this.client?.users.labels.get({
104 |             userId: params.email,
105 |             id: params.labelId
106 |           });
107 |           if (!response?.data) {
108 |             throw new GmailError(
109 |               'Label not found',
110 |               'NOT_FOUND_ERROR',
111 |               `Label ${params.labelId} does not exist`
112 |             );
113 |           }
114 |           return this.mapGmailLabel(response.data);
115 |         } else {
116 |           // Get all labels
117 |           return this.getLabels({ email: params.email });
118 |         }
119 | 
120 |       case 'update':
121 |         if (!params.labelId) {
122 |           throw new GmailError(
123 |             'Label ID is required for update',
124 |             'VALIDATION_ERROR',
125 |             'Please provide a label ID'
126 |           );
127 |         }
128 |         return this.updateLabel({
129 |           email: params.email,
130 |           labelId: params.labelId,
131 |           ...params.data
132 |         });
133 | 
134 |       case 'delete':
135 |         if (!params.labelId) {
136 |           throw new GmailError(
137 |             'Label ID is required for deletion',
138 |             'VALIDATION_ERROR',
139 |             'Please provide a label ID'
140 |           );
141 |         }
142 |         return this.deleteLabel({
143 |           email: params.email,
144 |           labelId: params.labelId
145 |         });
146 | 
147 |       default:
148 |         throw new GmailError(
149 |           'Invalid label action',
150 |           'VALIDATION_ERROR',
151 |           `Action ${params.action} is not supported`
152 |         );
153 |     }
154 |   }
155 | 
156 |   async manageLabelAssignment(params: ManageLabelAssignmentParams): Promise<void> {
157 |     this.ensureClient();
158 | 
159 |     const modifyParams: ModifyMessageLabelsParams = {
160 |       email: params.email,
161 |       messageId: params.messageId,
162 |       addLabelIds: params.action === 'add' ? params.labelIds : [],
163 |       removeLabelIds: params.action === 'remove' ? params.labelIds : []
164 |     };
165 | 
166 |     return this.modifyMessageLabels(modifyParams);
167 |   }
168 | 
169 |   /**
170 |    * Validate filter criteria to ensure all required fields are present and properly formatted
171 |    */
172 |   private validateFilterCriteria(criteria: LabelFilterCriteria): void {
173 |     if (!criteria) {
174 |       throw new GmailError(
175 |         'Filter criteria is required',
176 |         'VALIDATION_ERROR',
177 |         'Please provide filter criteria'
178 |       );
179 |     }
180 | 
181 |     // At least one filtering condition must be specified
182 |     const hasCondition = (criteria.from && criteria.from.length > 0) ||
183 |       (criteria.to && criteria.to.length > 0) ||
184 |       criteria.subject ||
185 |       (criteria.hasWords && criteria.hasWords.length > 0) ||
186 |       (criteria.doesNotHaveWords && criteria.doesNotHaveWords.length > 0) ||
187 |       criteria.hasAttachment ||
188 |       criteria.size;
189 | 
190 |     if (!hasCondition) {
191 |       throw new GmailError(
192 |         'Invalid filter criteria',
193 |         'VALIDATION_ERROR',
194 |         'At least one filtering condition must be specified (from, to, subject, hasWords, doesNotHaveWords, hasAttachment, or size)'
195 |       );
196 |     }
197 | 
198 |     // Validate email arrays
199 |     if (criteria.from?.length) {
200 |       criteria.from.forEach(email => {
201 |         if (!email.includes('@')) {
202 |           throw new GmailError(
203 |             'Invalid email address in from criteria',
204 |             'VALIDATION_ERROR',
205 |             `Invalid email address: ${email}`
206 |           );
207 |         }
208 |       });
209 |     }
210 | 
211 |     if (criteria.to?.length) {
212 |       criteria.to.forEach(email => {
213 |         if (!email.includes('@')) {
214 |           throw new GmailError(
215 |             'Invalid email address in to criteria',
216 |             'VALIDATION_ERROR',
217 |             `Invalid email address: ${email}`
218 |           );
219 |         }
220 |       });
221 |     }
222 | 
223 |     // Validate size criteria if present
224 |     if (criteria.size) {
225 |       if (typeof criteria.size.size !== 'number' || criteria.size.size <= 0) {
226 |         throw new GmailError(
227 |           'Invalid size criteria',
228 |           'VALIDATION_ERROR',
229 |           'Size must be a positive number'
230 |         );
231 |       }
232 |       if (!['larger', 'smaller'].includes(criteria.size.operator)) {
233 |         throw new GmailError(
234 |           'Invalid size operator',
235 |           'VALIDATION_ERROR',
236 |           'Size operator must be either "larger" or "smaller"'
237 |         );
238 |       }
239 |     }
240 |   }
241 | 
242 |   /**
243 |    * Build Gmail API query string from filter criteria
244 |    */
245 |   private buildFilterQuery(criteria: LabelFilterCriteria): string {
246 |     const conditions: string[] = [];
247 | 
248 |     if (criteria.from?.length) {
249 |       conditions.push(`{${criteria.from.map(email => `from:${email}`).join(' OR ')}}`);
250 |     }
251 | 
252 |     if (criteria.to?.length) {
253 |       conditions.push(`{${criteria.to.map(email => `to:${email}`).join(' OR ')}}`);
254 |     }
255 | 
256 |     if (criteria.subject) {
257 |       conditions.push(`subject:"${criteria.subject}"`);
258 |     }
259 | 
260 |     if (criteria.hasWords?.length) {
261 |       conditions.push(`{${criteria.hasWords.join(' OR ')}}`);
262 |     }
263 | 
264 |     if (criteria.doesNotHaveWords?.length) {
265 |       conditions.push(`-{${criteria.doesNotHaveWords.join(' OR ')}}`);
266 |     }
267 | 
268 |     if (criteria.hasAttachment) {
269 |       conditions.push('has:attachment');
270 |     }
271 | 
272 |     if (criteria.size) {
273 |       conditions.push(`size${criteria.size.operator === 'larger' ? '>' : '<'}${criteria.size.size}`);
274 |     }
275 | 
276 |     return conditions.join(' ');
277 |   }
278 | 
279 |   async manageLabelFilter(params: ManageLabelFilterParams): Promise<LabelFilter | GetLabelFiltersResponse | void> {
280 |     this.ensureClient();
281 | 
282 |     switch (params.action) {
283 |       case 'create':
284 |         if (!params.labelId) {
285 |           throw new GmailError(
286 |             'Label ID is required',
287 |             'VALIDATION_ERROR',
288 |             'Please provide a valid label ID'
289 |           );
290 |         }
291 |         if (!params.data?.criteria || !params.data?.actions) {
292 |           throw new GmailError(
293 |             'Filter configuration is required',
294 |             'VALIDATION_ERROR',
295 |             'Please provide both criteria and actions for the filter'
296 |           );
297 |         }
298 | 
299 |         // Validate filter criteria
300 |         this.validateFilterCriteria(params.data.criteria);
301 | 
302 |         return this.createLabelFilter({
303 |           email: params.email,
304 |           labelId: params.labelId,
305 |           criteria: params.data.criteria,
306 |           actions: params.data.actions
307 |         });
308 | 
309 |       case 'read':
310 |         return this.getLabelFilters({
311 |           email: params.email,
312 |           labelId: params.labelId
313 |         });
314 | 
315 |       case 'update':
316 |         if (!params.filterId || !params.labelId || !params.data?.criteria || !params.data?.actions) {
317 |           throw new GmailError(
318 |             'Missing required filter update data',
319 |             'VALIDATION_ERROR',
320 |             'Please provide filterId, labelId, criteria, and actions'
321 |           );
322 |         }
323 |         return this.updateLabelFilter({
324 |           email: params.email,
325 |           filterId: params.filterId,
326 |           labelId: params.labelId,
327 |           criteria: params.data.criteria,
328 |           actions: params.data.actions
329 |         });
330 | 
331 |       case 'delete':
332 |         if (!params.filterId) {
333 |           throw new GmailError(
334 |             'Filter ID is required for deletion',
335 |             'VALIDATION_ERROR',
336 |             'Please provide a filter ID'
337 |           );
338 |         }
339 |         return this.deleteLabelFilter({
340 |           email: params.email,
341 |           filterId: params.filterId
342 |         });
343 | 
344 |       default:
345 |         throw new GmailError(
346 |           'Invalid filter action',
347 |           'VALIDATION_ERROR',
348 |           `Action ${params.action} is not supported`
349 |         );
350 |     }
351 |   }
352 | 
353 |   // Helper methods that implement the actual operations
354 |   private async createLabel(params: CreateLabelParams): Promise<Label> {
355 |     try {
356 |       if (params.color) {
357 |         const { textColor, backgroundColor } = params.color;
358 |         if (!isValidGmailLabelColor(textColor, backgroundColor)) {
359 |           const suggestedColor = getNearestGmailLabelColor(backgroundColor);
360 |           throw new GmailError(
361 |             LABEL_ERROR_MESSAGES.INVALID_COLOR,
362 |             'COLOR_ERROR',
363 |             LABEL_ERROR_MESSAGES.COLOR_SUGGESTION(backgroundColor, suggestedColor)
364 |           );
365 |         }
366 |       }
367 | 
368 |       if (!this.client) {
369 |         throw new GmailError(
370 |           'Gmail client not initialized',
371 |           'CLIENT_ERROR',
372 |           'Please ensure the service is initialized with a valid client'
373 |         );
374 |       }
375 | 
376 |       const response = await this.client.users.labels.create({
377 |         userId: params.email,
378 |         requestBody: {
379 |           name: params.name,
380 |           messageListVisibility: params.messageListVisibility || 'show',
381 |           labelListVisibility: params.labelListVisibility || 'labelShow',
382 |           color: params.color && {
383 |             textColor: params.color.textColor,
384 |             backgroundColor: params.color.backgroundColor
385 |           }
386 |         }
387 |       });
388 | 
389 |       if (!response?.data) {
390 |         throw new GmailError(
391 |           'No response data from create label request',
392 |           'CREATE_ERROR',
393 |           'Server returned empty response'
394 |         );
395 |       }
396 | 
397 |       return this.mapGmailLabel(response.data);
398 |     } catch (error: unknown) {
399 |       if (error instanceof Error && 'code' in error && error.code === '401') {
400 |         throw new GmailError(
401 |           'Authentication failed',
402 |           'AUTH_ERROR',
403 |           'Please re-authenticate your account'
404 |         );
405 |       }
406 |       
407 |       if (error instanceof Error && error.message.includes('Invalid grant')) {
408 |         throw new GmailError(
409 |           'Authentication token expired',
410 |           'TOKEN_ERROR',
411 |           'Please re-authenticate your account'
412 |         );
413 |       }
414 | 
415 |       throw new GmailError(
416 |         'Failed to create label',
417 |         'CREATE_ERROR',
418 |         error instanceof Error ? error.message : 'Unknown error'
419 |       );
420 |     }
421 |   }
422 | 
423 |   private async getLabels(params: GetLabelsParams): Promise<GetLabelsResponse> {
424 |     try {
425 |       const response = await this.client?.users.labels.list({
426 |         userId: params.email
427 |       });
428 | 
429 |       if (!response?.data.labels) {
430 |         return { labels: [] };
431 |       }
432 | 
433 |       return {
434 |         labels: response.data.labels.map(this.mapGmailLabel)
435 |       };
436 |     } catch (error: unknown) {
437 |       throw new GmailError(
438 |         'Failed to fetch labels',
439 |         'FETCH_ERROR',
440 |         error instanceof Error ? error.message : 'Unknown error'
441 |       );
442 |     }
443 |   }
444 | 
445 |   private async updateLabel(params: UpdateLabelParams): Promise<Label> {
446 |     try {
447 |       if (params.color) {
448 |         const { textColor, backgroundColor } = params.color;
449 |         if (!isValidGmailLabelColor(textColor, backgroundColor)) {
450 |           const suggestedColor = getNearestGmailLabelColor(backgroundColor);
451 |           throw new GmailError(
452 |             LABEL_ERROR_MESSAGES.INVALID_COLOR,
453 |             'COLOR_ERROR',
454 |             LABEL_ERROR_MESSAGES.COLOR_SUGGESTION(backgroundColor, suggestedColor)
455 |           );
456 |         }
457 |       }
458 | 
459 |       if (!this.client) {
460 |         throw new GmailError(
461 |           'Gmail client not initialized',
462 |           'CLIENT_ERROR',
463 |           'Please ensure the service is initialized with a valid client'
464 |         );
465 |       }
466 | 
467 |       const response = await this.client.users.labels.patch({
468 |         userId: params.email,
469 |         id: params.labelId,
470 |         requestBody: {
471 |           name: params.name,
472 |           messageListVisibility: params.messageListVisibility,
473 |           labelListVisibility: params.labelListVisibility,
474 |           color: params.color && {
475 |             textColor: params.color.textColor,
476 |             backgroundColor: params.color.backgroundColor
477 |           }
478 |         }
479 |       });
480 | 
481 |       if (!response?.data) {
482 |         throw new GmailError(
483 |           'No response data from update label request',
484 |           'UPDATE_ERROR',
485 |           'Server returned empty response'
486 |         );
487 |       }
488 | 
489 |       return this.mapGmailLabel(response.data);
490 |     } catch (error: unknown) {
491 |       if (error instanceof Error && 'code' in error && error.code === '401') {
492 |         throw new GmailError(
493 |           'Authentication failed',
494 |           'AUTH_ERROR',
495 |           'Please re-authenticate your account'
496 |         );
497 |       }
498 |       
499 |       if (error instanceof Error && error.message.includes('Invalid grant')) {
500 |         throw new GmailError(
501 |           'Authentication token expired',
502 |           'TOKEN_ERROR',
503 |           'Please re-authenticate your account'
504 |         );
505 |       }
506 | 
507 |       throw new GmailError(
508 |         'Failed to update label',
509 |         'UPDATE_ERROR',
510 |         error instanceof Error ? error.message : 'Unknown error'
511 |       );
512 |     }
513 |   }
514 | 
515 |   private async deleteLabel(params: DeleteLabelParams): Promise<void> {
516 |     try {
517 |       await this.client?.users.labels.delete({
518 |         userId: params.email,
519 |         id: params.labelId
520 |       });
521 |     } catch (error: unknown) {
522 |       throw new GmailError(
523 |         'Failed to delete label',
524 |         'DELETE_ERROR',
525 |         error instanceof Error ? error.message : 'Unknown error'
526 |       );
527 |     }
528 |   }
529 | 
530 |   private async modifyMessageLabels(params: ModifyMessageLabelsParams): Promise<void> {
531 |     try {
532 |       await this.client?.users.messages.modify({
533 |         userId: params.email,
534 |         id: params.messageId,
535 |         requestBody: {
536 |           addLabelIds: params.addLabelIds,
537 |           removeLabelIds: params.removeLabelIds
538 |         }
539 |       });
540 |     } catch (error: unknown) {
541 |       throw new GmailError(
542 |         'Failed to modify message labels',
543 |         'MODIFY_ERROR',
544 |         error instanceof Error ? error.message : 'Unknown error'
545 |       );
546 |     }
547 |   }
548 | 
549 |   private async createLabelFilter(params: CreateLabelFilterParams): Promise<LabelFilter> {
550 |     try {
551 |       // Build filter criteria for Gmail API
552 |       const filterCriteria: gmail_v1.Schema$FilterCriteria = {
553 |         from: params.criteria.from?.join(' OR ') || undefined,
554 |         to: params.criteria.to?.join(' OR ') || undefined,
555 |         subject: params.criteria.subject || undefined,
556 |         query: this.buildFilterQuery(params.criteria),
557 |         hasAttachment: params.criteria.hasAttachment || undefined,
558 |         excludeChats: true,
559 |         size: params.criteria.size ? Number(params.criteria.size.size) : undefined,
560 |         sizeComparison: params.criteria.size?.operator || undefined
561 |       };
562 | 
563 |       // Build filter action with initialized arrays
564 |       const addLabelIds = [params.labelId];
565 |       const removeLabelIds: string[] = [];
566 | 
567 |       // Add system label modifications based on actions
568 |       if (params.actions.markImportant) {
569 |         addLabelIds.push('IMPORTANT');
570 |       }
571 |       if (params.actions.markRead) {
572 |         removeLabelIds.push('UNREAD');
573 |       }
574 |       if (params.actions.archive) {
575 |         removeLabelIds.push('INBOX');
576 |       }
577 | 
578 |       const filterAction: gmail_v1.Schema$FilterAction = {
579 |         addLabelIds,
580 |         removeLabelIds,
581 |         forward: undefined
582 |       };
583 | 
584 |       // Create the filter
585 |       const response = await this.client?.users.settings.filters.create({
586 |         userId: params.email,
587 |         requestBody: {
588 |           criteria: filterCriteria,
589 |           action: filterAction
590 |         }
591 |       });
592 | 
593 |       if (!response?.data) {
594 |         throw new GmailError(
595 |           'Failed to create filter',
596 |           'CREATE_ERROR',
597 |           'No response data received from Gmail API'
598 |         );
599 |       }
600 | 
601 |       // Return the created filter in our standard format
602 |       return {
603 |         id: response.data.id || '',
604 |         labelId: params.labelId,
605 |         criteria: params.criteria,
606 |         actions: params.actions
607 |       };
608 |     } catch (error: unknown) {
609 |       throw new GmailError(
610 |         'Failed to create label filter',
611 |         'CREATE_ERROR',
612 |         error instanceof Error ? error.message : 'Unknown error'
613 |       );
614 |     }
615 |   }
616 | 
617 |   private async getLabelFilters(params: GetLabelFiltersParams): Promise<GetLabelFiltersResponse> {
618 |     try {
619 |       const response = await this.client?.users.settings.filters.list({
620 |         userId: params.email
621 |       });
622 | 
623 |       if (!response?.data.filter) {
624 |         return { filters: [] };
625 |       }
626 | 
627 |       // Map Gmail API filters to our format
628 |       const filters = response.data.filter
629 |         .filter(filter => {
630 |           if (!filter.action?.addLabelIds?.length) return false;
631 |           // If labelId is provided, only return filters for that label
632 |           if (params.labelId) {
633 |             return filter.action.addLabelIds.includes(params.labelId);
634 |           }
635 |           return true;
636 |         })
637 |         .map(filter => ({
638 |           id: filter.id || '',
639 |           labelId: filter.action?.addLabelIds?.[0] || '',
640 |           criteria: {
641 |             from: filter.criteria?.from ? filter.criteria.from.split(' OR ') : undefined,
642 |             to: filter.criteria?.to ? filter.criteria.to.split(' OR ') : undefined,
643 |             subject: filter.criteria?.subject || undefined,
644 |             hasAttachment: filter.criteria?.hasAttachment || undefined,
645 |             hasWords: filter.criteria?.query ? [filter.criteria.query] : undefined,
646 |             doesNotHaveWords: filter.criteria?.negatedQuery ? [filter.criteria.negatedQuery] : undefined,
647 |             size: filter.criteria?.size && filter.criteria?.sizeComparison ? {
648 |               operator: filter.criteria.sizeComparison as 'larger' | 'smaller',
649 |               size: Number(filter.criteria.size)
650 |             } : undefined
651 |           },
652 |           actions: {
653 |             addLabel: true,
654 |             markImportant: filter.action?.addLabelIds?.includes('IMPORTANT') || false,
655 |             markRead: filter.action?.removeLabelIds?.includes('UNREAD') || false,
656 |             archive: filter.action?.removeLabelIds?.includes('INBOX') || false
657 |           }
658 |         }));
659 | 
660 |       return { filters };
661 |     } catch (error: unknown) {
662 |       throw new GmailError(
663 |         'Failed to get label filters',
664 |         'FETCH_ERROR',
665 |         error instanceof Error ? error.message : 'Unknown error'
666 |       );
667 |     }
668 |   }
669 | 
670 |   private async updateLabelFilter(params: UpdateLabelFilterParams): Promise<LabelFilter> {
671 |     try {
672 |       // Gmail API doesn't support direct filter updates, so we need to delete and recreate
673 |       await this.deleteLabelFilter({
674 |         email: params.email,
675 |         filterId: params.filterId
676 |       });
677 | 
678 |       // Convert our criteria format to Gmail API format
679 |       const criteria: gmail_v1.Schema$FilterCriteria = {
680 |         from: params.criteria.from?.join(' OR ') || null,
681 |         to: params.criteria.to?.join(' OR ') || null,
682 |         subject: params.criteria.subject || null,
683 |         query: params.criteria.hasWords?.join(' OR ') || null,
684 |         negatedQuery: params.criteria.doesNotHaveWords?.join(' OR ') || null,
685 |         hasAttachment: params.criteria.hasAttachment || null,
686 |         size: params.criteria.size?.size || null,
687 |         sizeComparison: params.criteria.size?.operator || null
688 |       };
689 | 
690 |       // Initialize arrays for label IDs
691 |       const addLabelIds: string[] = [params.labelId];
692 |       const removeLabelIds: string[] = [];
693 | 
694 |       // Add additional label IDs based on actions
695 |       if (params.actions.markImportant) {
696 |         addLabelIds.push('IMPORTANT');
697 |       }
698 |       if (params.actions.markRead) {
699 |         removeLabelIds.push('UNREAD');
700 |       }
701 |       if (params.actions.archive) {
702 |         removeLabelIds.push('INBOX');
703 |       }
704 | 
705 |       // Create the filter action
706 |       const action: gmail_v1.Schema$FilterAction = {
707 |         addLabelIds,
708 |         removeLabelIds,
709 |         forward: null
710 |       };
711 | 
712 |       const response = await this.client?.users.settings.filters.create({
713 |         userId: params.email,
714 |         requestBody: {
715 |           criteria,
716 |           action
717 |         }
718 |       });
719 |       if (!response?.data) {
720 |         throw new GmailError(
721 |           'No response data from update filter request',
722 |           'UPDATE_ERROR',
723 |           'Server returned empty response'
724 |         );
725 |       }
726 |       // Map response to our LabelFilter format
727 |       return {
728 |         id: response.data.id || '',
729 |         labelId: params.labelId,
730 |         criteria: params.criteria,
731 |         actions: params.actions
732 |       };
733 |     } catch (error: unknown) {
734 |       throw new GmailError(
735 |         'Failed to update label filter',
736 |         'UPDATE_ERROR',
737 |         error instanceof Error ? error.message : 'Unknown error'
738 |       );
739 |     }
740 |   }
741 | 
742 |   private async deleteLabelFilter(params: DeleteLabelFilterParams): Promise<void> {
743 |     try {
744 |       await this.client?.users.settings.filters.delete({
745 |         userId: params.email,
746 |         id: params.filterId
747 |       });
748 |     } catch (error: unknown) {
749 |       throw new GmailError(
750 |         'Failed to delete label filter',
751 |         'DELETE_ERROR',
752 |         error instanceof Error ? error.message : 'Unknown error'
753 |       );
754 |     }
755 |   }
756 | 
757 |   private mapGmailLabel(label: gmail_v1.Schema$Label): Label {
758 |     const mappedLabel: Label = {
759 |       id: label.id || '',
760 |       name: label.name || '',
761 |       type: (label.type || 'user') as 'system' | 'user',
762 |       messageListVisibility: (label.messageListVisibility || 'show') as 'hide' | 'show',
763 |       labelListVisibility: (label.labelListVisibility || 'labelShow') as 'labelHide' | 'labelShow' | 'labelShowIfUnread'
764 |     };
765 | 
766 |     if (label.color?.textColor && label.color?.backgroundColor) {
767 |       mappedLabel.color = {
768 |         textColor: label.color.textColor,
769 |         backgroundColor: label.color.backgroundColor
770 |       };
771 |     }
772 | 
773 |     return mappedLabel;
774 |   }
775 | }
776 | 
```

--------------------------------------------------------------------------------
/src/tools/definitions.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { ToolMetadata } from "../modules/tools/registry.js";
   2 | 
   3 | // Account Management Tools
   4 | export const accountTools: ToolMetadata[] = [
   5 |   {
   6 |     name: 'list_workspace_accounts',
   7 |     category: 'Account Management',
   8 |     description: `List all configured Google workspace accounts and their authentication status.
   9 |     
  10 |     IMPORTANT: This tool MUST be called first before any other workspace operations to:
  11 |     1. Check for existing authenticated accounts
  12 |     2. Determine which account to use if multiple exist
  13 |     3. Verify required API scopes are authorized
  14 |     
  15 |     Common Response Patterns:
  16 |     - Valid account exists → Proceed with requested operation
  17 |     - Multiple accounts exist → Ask user which to use
  18 |     - Token expired → Proceed normally (auto-refresh occurs)
  19 |     - No accounts exist → Start authentication flow
  20 |     
  21 |     Example Usage:
  22 |     1. User asks to "check email"
  23 |     2. Call this tool first to validate account access
  24 |     3. If account valid, proceed to email operations
  25 |     4. If multiple accounts, ask user "Which account would you like to use?"
  26 |     5. Remember chosen account for subsequent operations`,
  27 |     aliases: ['list_accounts', 'get_accounts', 'show_accounts'],
  28 |     inputSchema: {
  29 |       type: 'object',
  30 |       properties: {}
  31 |     }
  32 |   },
  33 |   {
  34 |     name: 'authenticate_workspace_account',
  35 |     category: 'Account Management',
  36 |     description: `Add and authenticate a Google account for API access.
  37 |     
  38 |     IMPORTANT: Only use this tool if list_workspace_accounts shows:
  39 |     1. No existing accounts, OR
  40 |     2. When using the account it seems to lack necessary auth scopes.
  41 |     
  42 |     To prevent wasted time, DO NOT use this tool:
  43 |     - Without checking list_workspace_accounts first
  44 |     - When token is just expired (auto-refresh handles this)
  45 |     - To re-authenticate an already valid account
  46 |     
  47 |     Steps to complete authentication:
  48 |     1. You call with required email address
  49 |     2. You receive auth_url in response
  50 |     3. You share EXACT auth_url with user - in a clickable URL form! (Important!)
  51 |     4. User completes OAuth flow by clicking on the link you furnished them
  52 |     5. User provides auth_code back to you
  53 |     6. Complete authentication with auth_code`,
  54 |     aliases: ['auth_account', 'add_account', 'connect_account'],
  55 |     inputSchema: {
  56 |       type: 'object',
  57 |       properties: {
  58 |         email: {
  59 |           type: 'string',
  60 |           description: 'Email address of the Google account to authenticate'
  61 |         },
  62 |         category: {
  63 |           type: 'string',
  64 |           description: 'Account category (e.g., work, personal)'
  65 |         },
  66 |         description: {
  67 |           type: 'string',
  68 |           description: 'Account description'
  69 |         },
  70 |         auth_code: {
  71 |           type: 'string',
  72 |           description: 'Authorization code from Google OAuth (for initial authentication)'
  73 |         },
  74 |         auto_complete: {
  75 |           type: 'boolean',
  76 |           description: 'Whether to use automatic authentication completion (default: true)'
  77 |         }
  78 |       },
  79 |       required: ['email']
  80 |     }
  81 |   },
  82 |   {
  83 |     name: 'complete_workspace_auth',
  84 |     category: 'Account Management',
  85 |     description: `Complete OAuth authentication automatically by waiting for callback.
  86 |     
  87 |     This tool waits for the user to complete OAuth authorization in their browser
  88 |     and automatically captures the authorization code when the callback is received.
  89 |     
  90 |     IMPORTANT: Only use this AFTER authenticate_workspace_account has returned an auth_url
  91 |     and the user has clicked on it to start the authorization process.
  92 |     
  93 |     The tool will wait up to 2 minutes for the authorization to complete.`,
  94 |     aliases: ['wait_for_auth', 'complete_auth'],
  95 |     inputSchema: {
  96 |       type: 'object',
  97 |       properties: {
  98 |         email: {
  99 |           type: 'string',
 100 |           description: 'Email address of the account being authenticated'
 101 |         }
 102 |       },
 103 |       required: ['email']
 104 |     }
 105 |   },
 106 |   {
 107 |     name: 'remove_workspace_account',
 108 |     category: 'Account Management',
 109 |     description: 'Remove a Google account and delete its associated authentication tokens',
 110 |     aliases: ['delete_account', 'disconnect_account', 'remove_account'],
 111 |     inputSchema: {
 112 |       type: 'object',
 113 |       properties: {
 114 |         email: {
 115 |           type: 'string',
 116 |           description: 'Email address of the Google account to remove'
 117 |         }
 118 |       },
 119 |       required: ['email']
 120 |     }
 121 |   }
 122 | ];
 123 | 
 124 | // Gmail Tools
 125 | export const gmailTools: ToolMetadata[] = [
 126 |   {
 127 |     name: 'manage_workspace_attachment',
 128 |     category: 'Gmail/Messages',
 129 |     description: `Manage attachments from Gmail messages and Calendar events using local storage.
 130 |     
 131 |     IMPORTANT: Before any operation:
 132 |     1. Verify account access with list_workspace_accounts
 133 |     2. Confirm account if multiple exist
 134 |     3. Validate required parameters
 135 |     
 136 |     Operations:
 137 |     - download: Download attachment to local storage
 138 |     - upload: Upload new attachment
 139 |     - delete: Remove attachment from storage
 140 |     
 141 |     Storage Location:
 142 |     - Files are stored in WORKSPACE_BASE_PATH/attachments
 143 |     - Email attachments: .../attachments/email/
 144 |     - Calendar attachments: .../attachments/calendar/
 145 |     
 146 |     Example Flow:
 147 |     1. Check account access
 148 |     2. Validate message and attachment exist
 149 |     3. Perform requested operation
 150 |     4. Return attachment metadata`,
 151 |     aliases: ['manage_attachment', 'handle_attachment', 'attachment_operation'],
 152 |     inputSchema: {
 153 |       type: 'object',
 154 |       properties: {
 155 |         email: {
 156 |           type: 'string',
 157 |           description: 'Email address of the workspace account'
 158 |         },
 159 |         action: {
 160 |           type: 'string',
 161 |           enum: ['download', 'upload', 'delete'],
 162 |           description: 'Action to perform on the attachment'
 163 |         },
 164 |         source: {
 165 |           type: 'string',
 166 |           enum: ['email', 'calendar'],
 167 |           description: 'Source type (email or calendar)'
 168 |         },
 169 |         messageId: {
 170 |           type: 'string',
 171 |           description: 'ID of the email/event containing the attachment'
 172 |         },
 173 |         filename: {
 174 |           type: 'string',
 175 |           description: 'Name of the attachment to manage'
 176 |         },
 177 |         content: {
 178 |           type: 'string',
 179 |           description: 'Base64 encoded file content (required for upload)'
 180 |         }
 181 |       },
 182 |       required: ['email', 'action', 'source', 'messageId', 'filename']
 183 |     }
 184 |   },
 185 |   {
 186 |     name: 'search_workspace_emails',
 187 |     category: 'Gmail/Messages',
 188 |     description: `Search emails in a Gmail account with advanced filtering capabilities.
 189 | 
 190 |     IMPORTANT: Before using this tool:
 191 |     1. Call list_workspace_accounts to verify account access
 192 |     2. If multiple accounts, confirm which account to use
 193 |     3. Check required scopes include Gmail read access
 194 |     
 195 |     Search Patterns:
 196 |     1. Simple Search:
 197 |        - Use individual criteria fields (from, to, subject)
 198 |        - Combine multiple conditions with AND logic
 199 |        - Example: from:[email protected] subject:"meeting"
 200 |     
 201 |     2. Complex Gmail Query:
 202 |        - Use content field for advanced Gmail search syntax
 203 |        - Supports full Gmail search operators
 204 |        - Example: "from:([email protected] OR [email protected]) subject:(meeting OR sync) -in:spam"
 205 |     
 206 |     Common Query Patterns:
 207 |     - Meeting emails: "from:(*@zoom.us OR zoom.us OR [email protected]) subject:(meeting OR sync OR invite)"
 208 |     - HR/Admin: "from:(*@workday.com OR *@adp.com) subject:(time off OR PTO OR benefits)"
 209 |     - Team updates: "from:(*@company.com) -from:([email protected])"
 210 |     - Newsletters: "subject:(newsletter OR digest) from:(*@company.com)"
 211 |     
 212 |     Search Tips:
 213 |     - Date format: YYYY-MM-DD (e.g., "2024-02-18")
 214 |     - Labels: Case-sensitive, exact match (e.g., "INBOX", "SENT")
 215 |     - Wildcards: Use * for partial matches (e.g., "*@domain.com")
 216 |     - Operators: OR, -, (), has:attachment, larger:size, newer_than:date
 217 |     - Default maxResults: 10 (increase for broader searches)`,
 218 |     aliases: ['search_emails', 'find_emails', 'query_emails'],
 219 |     inputSchema: {
 220 |       type: 'object',
 221 |       properties: {
 222 |         email: {
 223 |           type: 'string',
 224 |           description: 'Email address of the Gmail account'
 225 |         },
 226 |         search: {
 227 |           type: 'object',
 228 |           description: 'Search criteria for filtering emails',
 229 |           properties: {
 230 |             from: {
 231 |               oneOf: [
 232 |                 { type: 'string' },
 233 |                 { type: 'array', items: { type: 'string' } }
 234 |               ],
 235 |               description: 'Search by sender email address(es)'
 236 |             },
 237 |             to: {
 238 |               oneOf: [
 239 |                 { type: 'string' },
 240 |                 { type: 'array', items: { type: 'string' } }
 241 |               ],
 242 |               description: 'Search by recipient email address(es)'
 243 |             },
 244 |             subject: {
 245 |               type: 'string',
 246 |               description: 'Search in email subject lines'
 247 |             },
 248 |             content: {
 249 |               type: 'string',
 250 |               description: 'Complex Gmail search query with full operator support (e.g., "from:(alice OR bob) subject:(meeting OR sync)")'
 251 |             },
 252 |             after: {
 253 |               type: 'string',
 254 |               description: 'Search emails after this date in YYYY-MM-DD format (e.g., "2024-01-01")'
 255 |             },
 256 |             before: {
 257 |               type: 'string',
 258 |               description: 'Search emails before this date in YYYY-MM-DD format (e.g., "2024-12-31")'
 259 |             },
 260 |             hasAttachment: {
 261 |               type: 'boolean',
 262 |               description: 'Filter emails with attachments'
 263 |             },
 264 |             labels: {
 265 |               type: 'array',
 266 |               items: { type: 'string' },
 267 |               description: 'Include emails with these labels (e.g., INBOX, SENT, IMPORTANT)'
 268 |             },
 269 |             excludeLabels: {
 270 |               type: 'array',
 271 |               items: { type: 'string' },
 272 |               description: 'Exclude emails with these labels'
 273 |             },
 274 |             includeSpam: {
 275 |               type: 'boolean',
 276 |               description: 'Include emails from spam/trash folders'
 277 |             },
 278 |             isUnread: {
 279 |               type: 'boolean',
 280 |               description: 'Filter by read/unread status'
 281 |             }
 282 |           }
 283 |         },
 284 |         maxResults: {
 285 |           type: 'number',
 286 |           description: 'Maximum number of emails to return (default: 10)'
 287 |         }
 288 |       },
 289 |       required: ['email']
 290 |     }
 291 |   },
 292 |   {
 293 |     name: 'send_workspace_email',
 294 |     category: 'Gmail/Messages',
 295 |     description: `Send an email from a Gmail account.
 296 |     
 297 |     IMPORTANT: Before sending:
 298 |     0. Suggest writing a draft first, then send the draft.
 299 |     1. Verify account access with list_workspace_accounts
 300 |     2. Confirm sending account if multiple exist
 301 |     3. Validate all recipient addresses
 302 |     4. Check content for completeness
 303 |     
 304 |     Common Patterns:
 305 |     - Gather all required info before sending
 306 |     - Confirm critical details with user
 307 |     - Handle errors gracefully
 308 |     
 309 |     Example Flow:
 310 |     1. User requests to send email
 311 |     2. Check account access
 312 |     3. Collect recipient, subject, body
 313 |     4. Validate all fields
 314 |     5. Send and confirm success`,
 315 |     aliases: ['send_email', 'send_mail', 'create_email'],
 316 |     inputSchema: {
 317 |       type: 'object',
 318 |       properties: {
 319 |         email: {
 320 |           type: 'string',
 321 |           description: 'Email address to send from'
 322 |         },
 323 |         to: {
 324 |           type: 'array',
 325 |           items: { type: 'string' },
 326 |           description: 'List of recipient email addresses'
 327 |         },
 328 |         subject: {
 329 |           type: 'string',
 330 |           description: 'Email subject'
 331 |         },
 332 |         body: {
 333 |           type: 'string',
 334 |           description: 'Email body content'
 335 |         },
 336 |         cc: {
 337 |           type: 'array',
 338 |           items: { type: 'string' },
 339 |           description: 'List of CC recipient email addresses'
 340 |         },
 341 |         bcc: {
 342 |           type: 'array',
 343 |           items: { type: 'string' },
 344 |           description: 'List of BCC recipient email addresses'
 345 |         }
 346 |       },
 347 |       required: ['email', 'to', 'subject', 'body']
 348 |     }
 349 |   },
 350 |   {
 351 |     name: 'get_workspace_gmail_settings',
 352 |     category: 'Gmail/Settings',
 353 |     description: `Get Gmail settings and profile information for a workspace account.
 354 |     
 355 |     IMPORTANT: Always verify account access first with list_workspace_accounts.
 356 |     
 357 |     Common Uses:
 358 |     - Check account configuration
 359 |     - Verify email settings
 360 |     - Access profile information
 361 |     
 362 |     Response includes:
 363 |     - Language settings
 364 |     - Signature settings
 365 |     - Vacation responder status
 366 |     - Filters and forwarding
 367 |     - Other account preferences`,
 368 |     aliases: ['get_gmail_settings', 'gmail_settings', 'get_mail_settings'],
 369 |     inputSchema: {
 370 |       type: 'object',
 371 |       properties: {
 372 |         email: {
 373 |           type: 'string',
 374 |           description: 'Email address of the Gmail account'
 375 |         }
 376 |       },
 377 |       required: ['email']
 378 |     }
 379 |   },
 380 |   {
 381 |     name: 'manage_workspace_draft',
 382 |     category: 'Gmail/Drafts',
 383 |     description: `Manage Gmail drafts with CRUD operations and sending.
 384 |     
 385 |     IMPORTANT: Before any operation:
 386 |     1. Verify account access with list_workspace_accounts
 387 |     2. Confirm account if multiple exist
 388 |     3. Validate required data for operation
 389 |     
 390 |     Operations:
 391 |     - create: Create a new draft
 392 |     - read: Get a specific draft or list all drafts
 393 |     - update: Modify an existing draft
 394 |     - delete: Remove a draft
 395 |     - send: Send an existing draft
 396 |     
 397 |     Features:
 398 |     - New email drafts
 399 |     - Reply drafts with threading
 400 |     - Draft modifications
 401 |     - Draft sending
 402 |     
 403 |     Example Flow:
 404 |     1. Check account access
 405 |     2. Perform desired operation
 406 |     3. Confirm success`,
 407 |     aliases: ['manage_draft', 'draft_operation', 'handle_draft'],
 408 |     inputSchema: {
 409 |       type: 'object',
 410 |       properties: {
 411 |         email: {
 412 |           type: 'string',
 413 |           description: 'Email address of the Gmail account'
 414 |         },
 415 |         action: {
 416 |           type: 'string',
 417 |           enum: ['create', 'read', 'update', 'delete', 'send'],
 418 |           description: 'Operation to perform'
 419 |         },
 420 |         draftId: {
 421 |           type: 'string',
 422 |           description: 'Draft ID (required for read/update/delete/send)'
 423 |         },
 424 |         data: {
 425 |           type: 'object',
 426 |           properties: {
 427 |             to: {
 428 |               type: 'array',
 429 |               items: { type: 'string' },
 430 |               description: 'List of recipient email addresses'
 431 |             },
 432 |             subject: {
 433 |               type: 'string',
 434 |               description: 'Email subject'
 435 |             },
 436 |             body: {
 437 |               type: 'string',
 438 |               description: 'Email body content'
 439 |             },
 440 |             cc: {
 441 |               type: 'array',
 442 |               items: { type: 'string' },
 443 |               description: 'List of CC recipient email addresses'
 444 |             },
 445 |             bcc: {
 446 |               type: 'array',
 447 |               items: { type: 'string' },
 448 |               description: 'List of BCC recipient email addresses'
 449 |             },
 450 |             replyToMessageId: {
 451 |               type: 'string',
 452 |               description: 'Message ID to reply to (for creating reply drafts)'
 453 |             },
 454 |             threadId: {
 455 |               type: 'string',
 456 |               description: 'Thread ID for the email (optional for replies)'
 457 |             },
 458 |             references: {
 459 |               type: 'array',
 460 |               items: { type: 'string' },
 461 |               description: 'Reference message IDs for email threading'
 462 |             },
 463 |             inReplyTo: {
 464 |               type: 'string',
 465 |               description: 'Message ID being replied to (for email threading)'
 466 |             }
 467 |           }
 468 |         }
 469 |       },
 470 |       required: ['email', 'action']
 471 |     }
 472 |   }
 473 | ];
 474 | 
 475 | // Calendar Tools
 476 | export const calendarTools: ToolMetadata[] = [
 477 |   {
 478 |     name: 'list_workspace_calendar_events',
 479 |     category: 'Calendar/Events',
 480 |     description: `Get calendar events with optional filtering.
 481 |     
 482 |     IMPORTANT: Before listing events:
 483 |     1. Verify account access with list_workspace_accounts
 484 |     2. Confirm calendar account if multiple exist
 485 |     3. Check calendar access permissions
 486 |     
 487 |     Common Usage Patterns:
 488 |     - Default view: Current week's events
 489 |     - Specific range: Use timeMin/timeMax
 490 |     - Search: Use query for text search
 491 |     
 492 |     Example Flows:
 493 |     1. User asks "check my calendar":
 494 |        - Verify account access
 495 |        - Show current week by default
 496 |        - Include upcoming events
 497 |     
 498 |     2. User asks "find meetings about project":
 499 |        - Check account access
 500 |        - Search with relevant query
 501 |        - Focus on recent/upcoming events`,
 502 |     aliases: ['list_events', 'get_events', 'show_events'],
 503 |     inputSchema: {
 504 |       type: 'object',
 505 |       properties: {
 506 |         email: {
 507 |           type: 'string',
 508 |           description: 'Email address of the calendar owner'
 509 |         },
 510 |         query: {
 511 |           type: 'string',
 512 |           description: 'Optional text search within events'
 513 |         },
 514 |         maxResults: {
 515 |           type: 'number',
 516 |           description: 'Maximum number of events to return (default: 10)'
 517 |         },
 518 |         timeMin: {
 519 |           type: 'string',
 520 |           description: 'Start of time range to search (ISO date string)'
 521 |         },
 522 |         timeMax: {
 523 |           type: 'string',
 524 |           description: 'End of time range to search (ISO date string)'
 525 |         }
 526 |       },
 527 |       required: ['email']
 528 |     }
 529 |   },
 530 |   {
 531 |     name: 'get_workspace_calendar_event',
 532 |     category: 'Calendar/Events',
 533 |     description: 'Get a single calendar event by ID',
 534 |     aliases: ['get_event', 'view_event', 'show_event'],
 535 |     inputSchema: {
 536 |       type: 'object',
 537 |       properties: {
 538 |         email: {
 539 |           type: 'string',
 540 |           description: 'Email address of the calendar owner'
 541 |         },
 542 |         eventId: {
 543 |           type: 'string',
 544 |           description: 'Unique identifier of the event to retrieve'
 545 |         }
 546 |       },
 547 |       required: ['email', 'eventId']
 548 |     }
 549 |   },
 550 |   {
 551 |     name: 'manage_workspace_calendar_event',
 552 |     category: 'Calendar/Events',
 553 |     description: `Manage calendar event responses and updates including accept/decline, propose new times, and update event times.
 554 |     
 555 |     IMPORTANT: Before managing events:
 556 |     1. Verify account access with list_workspace_accounts
 557 |     2. Confirm calendar account if multiple exist
 558 |     3. Verify event exists and is modifiable
 559 |     
 560 |     Common Actions:
 561 |     - Accept/Decline invitations
 562 |     - Propose alternative times
 563 |     - Update existing events
 564 |     - Add comments to responses
 565 |     
 566 |     Example Flow:
 567 |     1. Check account access
 568 |     2. Verify event exists
 569 |     3. Perform desired action
 570 |     4. Confirm changes applied`,
 571 |     aliases: ['manage_event', 'update_event', 'respond_to_event'],
 572 |     inputSchema: {
 573 |       type: 'object',
 574 |       properties: {
 575 |         email: {
 576 |           type: 'string',
 577 |           description: 'Email address of the calendar owner'
 578 |         },
 579 |         eventId: {
 580 |           type: 'string',
 581 |           description: 'ID of the event to manage'
 582 |         },
 583 |         action: {
 584 |           type: 'string',
 585 |           enum: ['accept', 'decline', 'tentative', 'propose_new_time', 'update_time'],
 586 |           description: 'Action to perform on the event'
 587 |         },
 588 |         comment: {
 589 |           type: 'string',
 590 |           description: 'Optional comment to include with the response'
 591 |         },
 592 |         newTimes: {
 593 |           type: 'array',
 594 |           items: {
 595 |             type: 'object',
 596 |             properties: {
 597 |               start: {
 598 |                 type: 'object',
 599 |                 properties: {
 600 |                   dateTime: {
 601 |                     type: 'string',
 602 |                     description: 'Start time (ISO date string)'
 603 |                   },
 604 |                   timeZone: {
 605 |                     type: 'string',
 606 |                     description: 'Timezone for start time'
 607 |                   }
 608 |                 },
 609 |                 required: ['dateTime']
 610 |               },
 611 |               end: {
 612 |                 type: 'object',
 613 |                 properties: {
 614 |                   dateTime: {
 615 |                     type: 'string',
 616 |                     description: 'End time (ISO date string)'
 617 |                   },
 618 |                   timeZone: {
 619 |                     type: 'string',
 620 |                     description: 'Timezone for end time'
 621 |                   }
 622 |                 },
 623 |                 required: ['dateTime']
 624 |               }
 625 |             },
 626 |             required: ['start', 'end']
 627 |           },
 628 |           description: 'New proposed times for the event'
 629 |         }
 630 |       },
 631 |       required: ['email', 'eventId', 'action']
 632 |     }
 633 |   },
 634 |   {
 635 |     name: 'create_workspace_calendar_event',
 636 |     category: 'Calendar/Events',
 637 |     description: `Create a new calendar event.
 638 |     
 639 |     IMPORTANT: Before creating events:
 640 |     1. Verify account access with list_workspace_accounts
 641 |     2. Confirm calendar account if multiple exist
 642 |     3. Validate all required details
 643 |     
 644 |     Required Formats:
 645 |     - Times: ISO-8601 (e.g., "2024-02-18T15:30:00-06:00")
 646 |     - Timezone: IANA identifier (e.g., "America/Chicago")
 647 |     - Recurrence: RRULE format (e.g., "RRULE:FREQ=WEEKLY;COUNT=10")
 648 |     
 649 |     Common Patterns:
 650 |     1. Single Event:
 651 |        - Collect title, time, attendees
 652 |        - Check for conflicts
 653 |        - Create and confirm
 654 |     
 655 |     2. Recurring Event:
 656 |        - Validate recurrence pattern
 657 |        - Check series conflicts
 658 |        - Create with RRULE
 659 |     
 660 |     Response includes:
 661 |     - Created event ID
 662 |     - Scheduling conflicts
 663 |     - Attendee responses`,
 664 |     aliases: ['create_event', 'new_event', 'schedule_event'],
 665 |     inputSchema: {
 666 |       type: 'object',
 667 |       properties: {
 668 |         email: {
 669 |           type: 'string',
 670 |           description: 'Email address of the calendar owner'
 671 |         },
 672 |         summary: {
 673 |           type: 'string',
 674 |           description: 'Event title'
 675 |         },
 676 |         description: {
 677 |           type: 'string',
 678 |           description: 'Optional event description'
 679 |         },
 680 |         start: {
 681 |           type: 'object',
 682 |           properties: {
 683 |             dateTime: {
 684 |               type: 'string',
 685 |               description: 'Event start time as ISO-8601 string (e.g., "2024-02-18T15:30:00-06:00")'
 686 |             },
 687 |             timeZone: {
 688 |               type: 'string',
 689 |               description: 'IANA timezone identifier (e.g., "America/Chicago", "Europe/London")'
 690 |             }
 691 |           },
 692 |           required: ['dateTime']
 693 |         },
 694 |         end: {
 695 |           type: 'object',
 696 |           properties: {
 697 |             dateTime: {
 698 |               type: 'string',
 699 |               description: 'Event end time (ISO date string)'
 700 |             },
 701 |             timeZone: {
 702 |               type: 'string',
 703 |               description: 'Timezone for end time'
 704 |             }
 705 |           },
 706 |           required: ['dateTime']
 707 |         },
 708 |         attendees: {
 709 |           type: 'array',
 710 |           items: {
 711 |             type: 'object',
 712 |             properties: {
 713 |               email: {
 714 |                 type: 'string',
 715 |                 description: 'Attendee email address'
 716 |               }
 717 |             },
 718 |             required: ['email']
 719 |           },
 720 |           description: 'Optional list of event attendees'
 721 |         },
 722 |         recurrence: {
 723 |           type: 'array',
 724 |           items: { type: 'string' },
 725 |           description: 'RRULE strings for recurring events (e.g., ["RRULE:FREQ=WEEKLY"])'
 726 |         }
 727 |       },
 728 |       required: ['email', 'summary', 'start', 'end']
 729 |     }
 730 |   },
 731 |   {
 732 |     name: 'delete_workspace_calendar_event',
 733 |     category: 'Calendar/Events',
 734 |     description: `Delete a calendar event with options for recurring events.
 735 |     
 736 |     For recurring events, you can specify a deletion scope:
 737 |     - "entire_series": Removes all instances of the recurring event (default)
 738 |     - "this_and_following": Removes the selected instance and all future occurrences while preserving past instances
 739 |     
 740 |     This provides more granular control over calendar management and prevents accidental deletion of entire event series.`,
 741 |     aliases: ['delete_event', 'remove_event', 'cancel_event'],
 742 |     inputSchema: {
 743 |       type: 'object',
 744 |       properties: {
 745 |         email: {
 746 |           type: 'string',
 747 |           description: 'Email address of the calendar owner'
 748 |         },
 749 |         eventId: {
 750 |           type: 'string',
 751 |           description: 'ID of the event to delete'
 752 |         },
 753 |         sendUpdates: {
 754 |           type: 'string',
 755 |           enum: ['all', 'externalOnly', 'none'],
 756 |           description: 'Whether to send update notifications'
 757 |         },
 758 |         deletionScope: {
 759 |           type: 'string',
 760 |           enum: ['entire_series', 'this_and_following'],
 761 |           description: 'For recurring events, specifies which instances to delete'
 762 |         }
 763 |       },
 764 |       required: ['email', 'eventId']
 765 |     }
 766 |   }
 767 | ];
 768 | 
 769 | // Label Management Tools
 770 | export const labelTools: ToolMetadata[] = [
 771 |   {
 772 |     name: 'manage_workspace_label',
 773 |     category: 'Gmail/Labels',
 774 |     description: `Manage Gmail labels with CRUD operations.
 775 |     
 776 |     IMPORTANT: Before any operation:
 777 |     1. Verify account access with list_workspace_accounts
 778 |     2. Confirm account if multiple exist
 779 |     
 780 |     Operations:
 781 |     - create: Create a new label
 782 |     - read: Get a specific label or list all labels
 783 |     - update: Modify an existing label
 784 |     - delete: Remove a label
 785 |     
 786 |     Features:
 787 |     - Nested labels: Use "/" (e.g., "Work/Projects")
 788 |     - Custom colors: Hex codes (e.g., "#000000")
 789 |     - Visibility options: Show/hide in lists
 790 |     
 791 |     Limitations:
 792 |     - Cannot create/modify system labels (INBOX, SENT, SPAM)
 793 |     - Label names must be unique
 794 |     
 795 |     Example Flow:
 796 |     1. Check account access
 797 |     2. Perform desired operation
 798 |     3. Confirm success`,
 799 |     aliases: ['manage_label', 'label_operation', 'handle_label'],
 800 |     inputSchema: {
 801 |       type: 'object',
 802 |       properties: {
 803 |         email: {
 804 |           type: 'string',
 805 |           description: 'Email address of the Gmail account'
 806 |         },
 807 |         action: {
 808 |           type: 'string',
 809 |           enum: ['create', 'read', 'update', 'delete'],
 810 |           description: 'Operation to perform'
 811 |         },
 812 |         labelId: {
 813 |           type: 'string',
 814 |           description: 'Label ID (required for read/update/delete)'
 815 |         },
 816 |         data: {
 817 |           type: 'object',
 818 |           properties: {
 819 |             name: {
 820 |               type: 'string',
 821 |               description: 'Label name (required for create)'
 822 |             },
 823 |             messageListVisibility: {
 824 |               type: 'string',
 825 |               enum: ['show', 'hide'],
 826 |               description: 'Label visibility in message list'
 827 |             },
 828 |             labelListVisibility: {
 829 |               type: 'string',
 830 |               enum: ['labelShow', 'labelHide', 'labelShowIfUnread'],
 831 |               description: 'Label visibility in label list'
 832 |             },
 833 |             color: {
 834 |               type: 'object',
 835 |               properties: {
 836 |                 textColor: {
 837 |                   type: 'string',
 838 |                   description: 'Text color in hex format'
 839 |                 },
 840 |                 backgroundColor: {
 841 |                   type: 'string',
 842 |                   description: 'Background color in hex format'
 843 |                 }
 844 |               }
 845 |             }
 846 |           }
 847 |         }
 848 |       },
 849 |       required: ['email', 'action']
 850 |     }
 851 |   },
 852 |   {
 853 |     name: 'manage_workspace_label_assignment',
 854 |     category: 'Gmail/Labels',
 855 |     description: `Manage label assignments for Gmail messages.
 856 |     
 857 |     IMPORTANT: Before assigning:
 858 |     1. Verify account access with list_workspace_accounts
 859 |     2. Confirm account if multiple exist
 860 |     3. Verify message exists
 861 |     4. Check label validity
 862 |     
 863 |     Operations:
 864 |     - add: Apply labels to a message
 865 |     - remove: Remove labels from a message
 866 |     
 867 |     Common Use Cases:
 868 |     - Apply single label
 869 |     - Remove single label
 870 |     - Batch modify multiple labels
 871 |     - Update system labels (e.g., mark as read)
 872 |     
 873 |     Example Flow:
 874 |     1. Check account access
 875 |     2. Verify message and labels exist
 876 |     3. Apply requested changes
 877 |     4. Confirm modifications`,
 878 |     aliases: ['assign_label', 'modify_message_labels', 'change_message_labels'],
 879 |     inputSchema: {
 880 |       type: 'object',
 881 |       properties: {
 882 |         email: {
 883 |           type: 'string',
 884 |           description: 'Email address of the Gmail account'
 885 |         },
 886 |         action: {
 887 |           type: 'string',
 888 |           enum: ['add', 'remove'],
 889 |           description: 'Whether to add or remove labels'
 890 |         },
 891 |         messageId: {
 892 |           type: 'string',
 893 |           description: 'ID of the message to modify'
 894 |         },
 895 |         labelIds: {
 896 |           type: 'array',
 897 |           items: { type: 'string' },
 898 |           description: 'Array of label IDs to add or remove'
 899 |         }
 900 |       },
 901 |       required: ['email', 'action', 'messageId', 'labelIds']
 902 |     }
 903 |   },
 904 |   {
 905 |     name: 'manage_workspace_label_filter',
 906 |     category: 'Gmail/Labels',
 907 |     description: `Manage Gmail label filters with CRUD operations.
 908 |     
 909 |     IMPORTANT: Before any operation:
 910 |     1. Verify account access with list_workspace_accounts
 911 |     2. Confirm account if multiple exist
 912 |     3. Verify label exists for create/update
 913 |     4. Validate filter criteria
 914 |     
 915 |     Operations:
 916 |     - create: Create a new filter
 917 |     - read: Get filters (all or by label)
 918 |     - update: Modify existing filter
 919 |     - delete: Remove filter
 920 |     
 921 |     Filter Capabilities:
 922 |     - Match sender(s) and recipient(s)
 923 |     - Search subject and content
 924 |     - Filter by attachments
 925 |     - Size-based filtering
 926 |     
 927 |     Actions Available:
 928 |     - Apply label automatically
 929 |     - Mark as important
 930 |     - Mark as read
 931 |     - Archive message
 932 |     
 933 |     Criteria Format:
 934 |     1. Simple filters:
 935 |        - from: Array of email addresses
 936 |        - to: Array of email addresses
 937 |        - subject: String for exact match
 938 |        - hasAttachment: Boolean
 939 |     
 940 |     2. Complex queries:
 941 |        - hasWords: Array of query strings
 942 |        - doesNotHaveWords: Array of exclusion strings
 943 |     
 944 |     Example Flow:
 945 |     1. Check account access
 946 |     2. Validate criteria
 947 |     3. Perform operation
 948 |     4. Verify result`,
 949 |     aliases: ['manage_filter', 'handle_filter', 'filter_operation'],
 950 |     inputSchema: {
 951 |       type: 'object',
 952 |       properties: {
 953 |         email: {
 954 |           type: 'string',
 955 |           description: 'Email address of the Gmail account'
 956 |         },
 957 |         action: {
 958 |           type: 'string',
 959 |           enum: ['create', 'read', 'update', 'delete'],
 960 |           description: 'Operation to perform'
 961 |         },
 962 |         filterId: {
 963 |           type: 'string',
 964 |           description: 'Filter ID (required for update/delete)'
 965 |         },
 966 |         labelId: {
 967 |           type: 'string',
 968 |           description: 'Label ID (required for create/update)'
 969 |         },
 970 |         data: {
 971 |           type: 'object',
 972 |           properties: {
 973 |             criteria: {
 974 |               type: 'object',
 975 |               properties: {
 976 |                 from: {
 977 |                   type: 'array',
 978 |                   items: { type: 'string' },
 979 |                   description: 'Match sender email addresses'
 980 |                 },
 981 |                 to: {
 982 |                   type: 'array',
 983 |                   items: { type: 'string' },
 984 |                   description: 'Match recipient email addresses'
 985 |                 },
 986 |                 subject: {
 987 |                   type: 'string',
 988 |                   description: 'Match text in subject'
 989 |                 },
 990 |                 hasWords: {
 991 |                   type: 'array',
 992 |                   items: { type: 'string' },
 993 |                   description: 'Match words in message body'
 994 |                 },
 995 |                 doesNotHaveWords: {
 996 |                   type: 'array',
 997 |                   items: { type: 'string' },
 998 |                   description: 'Exclude messages with these words'
 999 |                 },
1000 |                 hasAttachment: {
1001 |                   type: 'boolean',
1002 |                   description: 'Match messages with attachments'
1003 |                 },
1004 |                 size: {
1005 |                   type: 'object',
1006 |                   properties: {
1007 |                     operator: {
1008 |                       type: 'string',
1009 |                       enum: ['larger', 'smaller'],
1010 |                       description: 'Size comparison operator'
1011 |                     },
1012 |                     size: {
1013 |                       type: 'number',
1014 |                       description: 'Size in bytes'
1015 |                     }
1016 |                   }
1017 |                 }
1018 |               }
1019 |             },
1020 |             actions: {
1021 |               type: 'object',
1022 |               properties: {
1023 |                 addLabel: {
1024 |                   type: 'boolean',
1025 |                   description: 'Apply the label'
1026 |                 },
1027 |                 markImportant: {
1028 |                   type: 'boolean',
1029 |                   description: 'Mark as important'
1030 |                 },
1031 |                 markRead: {
1032 |                   type: 'boolean',
1033 |                   description: 'Mark as read'
1034 |                 },
1035 |                 archive: {
1036 |                   type: 'boolean',
1037 |                   description: 'Archive the message'
1038 |                 }
1039 |               },
1040 |               required: ['addLabel']
1041 |             }
1042 |           }
1043 |         }
1044 |       },
1045 |       required: ['email', 'action']
1046 |     }
1047 |   }
1048 | ];
1049 | 
1050 | // Drive Tools
1051 | export const driveTools: ToolMetadata[] = [
1052 |   {
1053 |     name: 'list_drive_files',
1054 |     category: 'Drive/Files',
1055 |     description: `List files in a Google Drive account with optional filtering.
1056 |     
1057 |     IMPORTANT: Before listing files:
1058 |     1. Verify account access with list_workspace_accounts
1059 |     2. Confirm account if multiple exist
1060 |     3. Check Drive read permissions
1061 |     
1062 |     Common Usage Patterns:
1063 |     - List all files: No options needed
1064 |     - List folder contents: Provide folderId
1065 |     - Custom queries: Use query parameter
1066 |     
1067 |     Example Flow:
1068 |     1. Check account access
1069 |     2. Apply any filters
1070 |     3. Return file list with metadata`,
1071 |     aliases: ['list_files', 'get_files', 'show_files'],
1072 |     inputSchema: {
1073 |       type: 'object',
1074 |       properties: {
1075 |         email: {
1076 |           type: 'string',
1077 |           description: 'Email address of the Drive account'
1078 |         },
1079 |         options: {
1080 |           type: 'object',
1081 |           properties: {
1082 |             folderId: {
1083 |               type: 'string',
1084 |               description: 'Optional folder ID to list contents of'
1085 |             },
1086 |             query: {
1087 |               type: 'string',
1088 |               description: 'Custom query string for filtering'
1089 |             },
1090 |             pageSize: {
1091 |               type: 'number',
1092 |               description: 'Maximum number of files to return'
1093 |             },
1094 |             orderBy: {
1095 |               type: 'array',
1096 |               items: { type: 'string' },
1097 |               description: 'Sort order fields'
1098 |             },
1099 |             fields: {
1100 |               type: 'array',
1101 |               items: { type: 'string' },
1102 |               description: 'Fields to include in response'
1103 |             }
1104 |           }
1105 |         }
1106 |       },
1107 |       required: ['email']
1108 |     }
1109 |   },
1110 |   {
1111 |     name: 'search_drive_files',
1112 |     category: 'Drive/Files',
1113 |     description: `Search for files in Google Drive with advanced filtering.
1114 |     
1115 |     IMPORTANT: Before searching:
1116 |     1. Verify account access with list_workspace_accounts
1117 |     2. Confirm account if multiple exist
1118 |     3. Check Drive read permissions
1119 |     
1120 |     Search Capabilities:
1121 |     - Full text search across file content
1122 |     - Filter by MIME type
1123 |     - Filter by folder
1124 |     - Include/exclude trashed files
1125 |     
1126 |     Example Flow:
1127 |     1. Check account access
1128 |     2. Apply search criteria
1129 |     3. Return matching files`,
1130 |     aliases: ['search_files', 'find_files', 'query_files'],
1131 |     inputSchema: {
1132 |       type: 'object',
1133 |       properties: {
1134 |         email: {
1135 |           type: 'string',
1136 |           description: 'Email address of the Drive account'
1137 |         },
1138 |         options: {
1139 |           type: 'object',
1140 |           properties: {
1141 |             fullText: {
1142 |               type: 'string',
1143 |               description: 'Full text search query'
1144 |             },
1145 |             mimeType: {
1146 |               type: 'string',
1147 |               description: 'Filter by MIME type'
1148 |             },
1149 |             folderId: {
1150 |               type: 'string',
1151 |               description: 'Filter by parent folder ID'
1152 |             },
1153 |             trashed: {
1154 |               type: 'boolean',
1155 |               description: 'Include trashed files'
1156 |             },
1157 |             query: {
1158 |               type: 'string',
1159 |               description: 'Additional query string'
1160 |             },
1161 |             pageSize: {
1162 |               type: 'number',
1163 |               description: 'Maximum number of files to return'
1164 |             }
1165 |           }
1166 |         }
1167 |       },
1168 |       required: ['email', 'options']
1169 |     }
1170 |   },
1171 |   {
1172 |     name: 'upload_drive_file',
1173 |     category: 'Drive/Files',
1174 |     description: `Upload a file to Google Drive.
1175 |     
1176 |     IMPORTANT: Before uploading:
1177 |     1. Verify account access with list_workspace_accounts
1178 |     2. Confirm account if multiple exist
1179 |     3. Check Drive write permissions
1180 |     
1181 |     Features:
1182 |     - Specify file name and type
1183 |     - Place in specific folder
1184 |     - Set file metadata
1185 |     
1186 |     Example Flow:
1187 |     1. Check account access
1188 |     2. Validate file data
1189 |     3. Upload and return file info`,
1190 |     aliases: ['upload_file', 'create_file', 'add_file'],
1191 |     inputSchema: {
1192 |       type: 'object',
1193 |       properties: {
1194 |         email: {
1195 |           type: 'string',
1196 |           description: 'Email address of the Drive account'
1197 |         },
1198 |         options: {
1199 |           type: 'object',
1200 |           properties: {
1201 |             name: {
1202 |               type: 'string',
1203 |               description: 'Name for the uploaded file'
1204 |             },
1205 |             content: {
1206 |               type: 'string',
1207 |               description: 'File content (string or base64)'
1208 |             },
1209 |             mimeType: {
1210 |               type: 'string',
1211 |               description: 'MIME type of the file'
1212 |             },
1213 |             parents: {
1214 |               type: 'array',
1215 |               items: { type: 'string' },
1216 |               description: 'Parent folder IDs'
1217 |             }
1218 |           },
1219 |           required: ['name', 'content']
1220 |         }
1221 |       },
1222 |       required: ['email', 'options']
1223 |     }
1224 |   },
1225 |   {
1226 |     name: 'download_drive_file',
1227 |     category: 'Drive/Files',
1228 |     description: `Download a file from Google Drive.
1229 |     
1230 |     IMPORTANT: Before downloading:
1231 |     1. Verify account access with list_workspace_accounts
1232 |     2. Confirm account if multiple exist
1233 |     3. Check Drive read permissions
1234 |     
1235 |     Features:
1236 |     - Download any file type
1237 |     - Export Google Workspace files
1238 |     - Specify export format
1239 |     
1240 |     Example Flow:
1241 |     1. Check account access
1242 |     2. Validate file exists
1243 |     3. Download and return content`,
1244 |     aliases: ['download_file', 'get_file_content', 'fetch_file'],
1245 |     inputSchema: {
1246 |       type: 'object',
1247 |       properties: {
1248 |         email: {
1249 |           type: 'string',
1250 |           description: 'Email address of the Drive account'
1251 |         },
1252 |         fileId: {
1253 |           type: 'string',
1254 |           description: 'ID of the file to download'
1255 |         },
1256 |         mimeType: {
1257 |           type: 'string',
1258 |           description: 'Optional MIME type for export format'
1259 |         }
1260 |       },
1261 |       required: ['email', 'fileId']
1262 |     }
1263 |   },
1264 |   {
1265 |     name: 'create_drive_folder',
1266 |     category: 'Drive/Folders',
1267 |     description: `Create a new folder in Google Drive.
1268 |     
1269 |     IMPORTANT: Before creating:
1270 |     1. Verify account access with list_workspace_accounts
1271 |     2. Confirm account if multiple exist
1272 |     3. Check Drive write permissions
1273 |     
1274 |     Features:
1275 |     - Create in root or subfolder
1276 |     - Set folder metadata
1277 |     
1278 |     Example Flow:
1279 |     1. Check account access
1280 |     2. Validate folder name
1281 |     3. Create and return folder info`,
1282 |     aliases: ['create_folder', 'new_folder', 'add_folder'],
1283 |     inputSchema: {
1284 |       type: 'object',
1285 |       properties: {
1286 |         email: {
1287 |           type: 'string',
1288 |           description: 'Email address of the Drive account'
1289 |         },
1290 |         name: {
1291 |           type: 'string',
1292 |           description: 'Name for the new folder'
1293 |         },
1294 |         parentId: {
1295 |           type: 'string',
1296 |           description: 'Optional parent folder ID'
1297 |         }
1298 |       },
1299 |       required: ['email', 'name']
1300 |     }
1301 |   },
1302 |   {
1303 |     name: 'update_drive_permissions',
1304 |     category: 'Drive/Permissions',
1305 |     description: `Update sharing permissions for a Drive file or folder.
1306 |     
1307 |     IMPORTANT: Before updating:
1308 |     1. Verify account access with list_workspace_accounts
1309 |     2. Confirm account if multiple exist
1310 |     3. Check Drive sharing permissions
1311 |     
1312 |     Permission Types:
1313 |     - User: Share with specific email
1314 |     - Group: Share with Google Group
1315 |     - Domain: Share with entire domain
1316 |     - Anyone: Public sharing
1317 |     
1318 |     Roles:
1319 |     - owner: Full ownership rights
1320 |     - organizer: Organizational rights
1321 |     - fileOrganizer: File organization rights
1322 |     - writer: Edit access
1323 |     - commenter: Comment access
1324 |     - reader: View access
1325 |     
1326 |     Example Flow:
1327 |     1. Check account access
1328 |     2. Validate permission details
1329 |     3. Update and return result`,
1330 |     aliases: ['share_file', 'update_sharing', 'modify_permissions'],
1331 |     inputSchema: {
1332 |       type: 'object',
1333 |       properties: {
1334 |         email: {
1335 |           type: 'string',
1336 |           description: 'Email address of the Drive account'
1337 |         },
1338 |         options: {
1339 |           type: 'object',
1340 |           properties: {
1341 |             fileId: {
1342 |               type: 'string',
1343 |               description: 'ID of file/folder to update'
1344 |             },
1345 |             role: {
1346 |               type: 'string',
1347 |               enum: ['owner', 'organizer', 'fileOrganizer', 'writer', 'commenter', 'reader'],
1348 |               description: 'Permission role to grant'
1349 |             },
1350 |             type: {
1351 |               type: 'string',
1352 |               enum: ['user', 'group', 'domain', 'anyone'],
1353 |               description: 'Type of permission'
1354 |             },
1355 |             emailAddress: {
1356 |               type: 'string',
1357 |               description: 'Email address for user/group sharing'
1358 |             },
1359 |             domain: {
1360 |               type: 'string',
1361 |               description: 'Domain for domain sharing'
1362 |             },
1363 |             allowFileDiscovery: {
1364 |               type: 'boolean',
1365 |               description: 'Allow file discovery for anyone sharing'
1366 |             }
1367 |           },
1368 |           required: ['fileId', 'role', 'type']
1369 |         }
1370 |       },
1371 |       required: ['email', 'options']
1372 |     }
1373 |   },
1374 |   {
1375 |     name: 'delete_drive_file',
1376 |     category: 'Drive/Files',
1377 |     description: `Delete a file or folder from Google Drive.
1378 |     
1379 |     IMPORTANT: Before deleting:
1380 |     1. Verify account access with list_workspace_accounts
1381 |     2. Confirm account if multiple exist
1382 |     3. Check Drive write permissions
1383 |     4. Confirm deletion is intended
1384 |     
1385 |     Example Flow:
1386 |     1. Check account access
1387 |     2. Validate file exists
1388 |     3. Delete and confirm`,
1389 |     aliases: ['delete_file', 'remove_file', 'trash_file'],
1390 |     inputSchema: {
1391 |       type: 'object',
1392 |       properties: {
1393 |         email: {
1394 |           type: 'string',
1395 |           description: 'Email address of the Drive account'
1396 |         },
1397 |         fileId: {
1398 |           type: 'string',
1399 |           description: 'ID of the file/folder to delete'
1400 |         }
1401 |       },
1402 |       required: ['email', 'fileId']
1403 |     }
1404 |   }
1405 | ];
1406 | 
1407 | // Define Contacts Tools
1408 | export const contactsTools: ToolMetadata[] = [
1409 |   {
1410 |     name: "get_workspace_contacts",
1411 |     category: "Contacts",
1412 |     description: `Retrieve contacts from a Google account.
1413 | 
1414 |     IMPORTANT: Before using this tool:
1415 |     1. Verify account access with list_workspace_accounts
1416 |     2. Confirm account if multiple exist
1417 |     3. Check required scopes include Contacts read access
1418 | 
1419 |     Parameters:
1420 |     - email: The Google account email to access contacts from
1421 |     - personFields: Required fields to include in the response (e.g. "names,emailAddresses,phoneNumbers")
1422 |     - pageSize: Optional maximum number of contacts to return
1423 |     - pageToken: Optional token for pagination (to get the next page)
1424 | 
1425 |     Example Usage:
1426 |     1. Call list_workspace_accounts to check for valid accounts
1427 |     2. Call get_workspace_contacts with required parameters
1428 |     3. Process results and use pagination for large contact lists
1429 | 
1430 |     Common personFields Values:
1431 |     - Basic info: "names,emailAddresses,phoneNumbers"
1432 |     - Extended: "names,emailAddresses,phoneNumbers,addresses,organizations"
1433 |     - All data: "names,emailAddresses,phoneNumbers,addresses,organizations,biographies,birthdays,photos"`,
1434 |     aliases: ["get_contacts", "list_contacts", "fetch_contacts"],
1435 |     inputSchema: {
1436 |       type: "object",
1437 |       properties: {
1438 |         email: {
1439 |           type: "string",
1440 |           description: "Email address of the Google account"
1441 |         },
1442 |         personFields: {
1443 |           type: "string",
1444 |           description: 'Comma-separated fields to include in the response (e.g. "names,emailAddresses,phoneNumbers")'
1445 |         },
1446 |         pageSize: {
1447 |           type: "number",
1448 |           description: "Maximum number of contacts to return (default: 100)"
1449 |         },
1450 |         pageToken: {
1451 |           type: "string",
1452 |           description: "Page token from a previous response (for pagination)"
1453 |         }
1454 |       },
1455 |       required: ["email", "personFields"]
1456 |     }
1457 |   }
1458 | ];
1459 | 
1460 | // Export all tools combined
1461 | export const allTools: ToolMetadata[] = [
1462 |   ...accountTools,
1463 |   ...gmailTools,
1464 |   ...calendarTools,
1465 |   ...labelTools,
1466 |   ...driveTools,
1467 |   ...contactsTools
1468 | ];
1469 | 
```
Page 4/4FirstPrevNextLast