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 |
```