This is page 3 of 3. Use http://codebase.md/aaronsb/google-workspace-mcp?page={x} to view the full context.
# Directory Structure
```
├── .dockerignore
├── .eslintrc.json
├── .github
│ ├── config.yml
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── pull_request_template.md
│ └── workflows
│ ├── ci.yml
│ └── docker-publish.yml
├── .gitignore
├── ARCHITECTURE.md
├── cline_docs
│ ├── activeContext.md
│ ├── productContext.md
│ ├── progress.md
│ ├── systemPatterns.md
│ └── techContext.md
├── CODE_OF_CONDUCT.md
├── config
│ ├── accounts.example.json
│ ├── credentials
│ │ └── README.md
│ └── gauth.example.json
├── CONTRIBUTING.md
├── docker-entrypoint.sh
├── Dockerfile
├── Dockerfile.local
├── docs
│ ├── API.md
│ ├── assets
│ │ └── robot-assistant.png
│ ├── automatic-oauth-flow.md
│ ├── ERRORS.md
│ ├── EXAMPLES.md
│ └── TOOL_DISCOVERY.md
├── jest.config.cjs
├── jest.setup.cjs
├── LICENSE
├── llms-install.md
├── package-lock.json
├── package.json
├── README.md
├── scripts
│ ├── build-local.sh
│ └── local-entrypoint.sh
├── SECURITY.md
├── smithery.yaml
├── src
│ ├── __fixtures__
│ │ └── accounts.ts
│ ├── __helpers__
│ │ ├── package.json
│ │ └── testSetup.ts
│ ├── __mocks__
│ │ ├── @modelcontextprotocol
│ │ │ ├── sdk
│ │ │ │ ├── server
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── stdio.js
│ │ │ │ └── types.ts
│ │ │ └── sdk.ts
│ │ ├── googleapis.ts
│ │ └── logger.ts
│ ├── __tests__
│ │ └── modules
│ │ ├── accounts
│ │ │ ├── manager.test.ts
│ │ │ └── token.test.ts
│ │ ├── attachments
│ │ │ └── index.test.ts
│ │ ├── calendar
│ │ │ └── service.test.ts
│ │ └── gmail
│ │ └── service.test.ts
│ ├── api
│ │ ├── handler.ts
│ │ ├── request.ts
│ │ └── validators
│ │ ├── endpoint.ts
│ │ └── parameter.ts
│ ├── index.ts
│ ├── modules
│ │ ├── accounts
│ │ │ ├── callback-server.ts
│ │ │ ├── index.ts
│ │ │ ├── manager.ts
│ │ │ ├── oauth.ts
│ │ │ ├── token.ts
│ │ │ └── types.ts
│ │ ├── attachments
│ │ │ ├── cleanup-service.ts
│ │ │ ├── index-service.ts
│ │ │ ├── response-transformer.ts
│ │ │ ├── service.ts
│ │ │ ├── transformer.ts
│ │ │ └── types.ts
│ │ ├── calendar
│ │ │ ├── __tests__
│ │ │ │ └── scopes.test.ts
│ │ │ ├── index.ts
│ │ │ ├── scopes.ts
│ │ │ ├── service.ts
│ │ │ └── types.ts
│ │ ├── contacts
│ │ │ ├── index.ts
│ │ │ ├── scopes.ts
│ │ │ └── types.ts
│ │ ├── drive
│ │ │ ├── __tests__
│ │ │ │ ├── scopes.test.ts
│ │ │ │ └── service.test.ts
│ │ │ ├── index.ts
│ │ │ ├── scopes.ts
│ │ │ ├── service.ts
│ │ │ └── types.ts
│ │ ├── gmail
│ │ │ ├── __tests__
│ │ │ │ ├── label.test.ts
│ │ │ │ └── scopes.test.ts
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── scopes.ts
│ │ │ ├── service.ts
│ │ │ ├── services
│ │ │ │ ├── attachment.ts
│ │ │ │ ├── base.ts
│ │ │ │ ├── draft.ts
│ │ │ │ ├── email.ts
│ │ │ │ ├── label.ts
│ │ │ │ ├── search.ts
│ │ │ │ └── settings.ts
│ │ │ └── types.ts
│ │ └── tools
│ │ ├── __tests__
│ │ │ ├── registry.test.ts
│ │ │ └── scope-registry.test.ts
│ │ ├── registry.ts
│ │ └── scope-registry.ts
│ ├── oauth
│ │ └── client.ts
│ ├── scripts
│ │ ├── health-check.ts
│ │ ├── setup-environment.ts
│ │ └── setup-google-env.ts
│ ├── services
│ │ ├── base
│ │ │ └── BaseGoogleService.ts
│ │ ├── calendar
│ │ │ └── index.ts
│ │ ├── contacts
│ │ │ └── index.ts
│ │ ├── drive
│ │ │ └── index.ts
│ │ └── gmail
│ │ └── index.ts
│ ├── tools
│ │ ├── account-handlers.ts
│ │ ├── calendar-handlers.ts
│ │ ├── contacts-handlers.ts
│ │ ├── definitions.ts
│ │ ├── drive-handlers.ts
│ │ ├── gmail-handlers.ts
│ │ ├── server.ts
│ │ ├── type-guards.ts
│ │ └── types.ts
│ ├── types
│ │ └── modelcontextprotocol__sdk.d.ts
│ ├── types.ts
│ └── utils
│ ├── account.ts
│ ├── logger.ts
│ ├── service-initializer.ts
│ ├── token.ts
│ └── workspace.ts
├── TODO.md
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/src/modules/accounts/manager.ts:
--------------------------------------------------------------------------------
```typescript
import fs from 'fs/promises';
import path from 'path';
import { Account, AccountsConfig, AccountError, AccountModuleConfig } from './types.js';
import { scopeRegistry } from '../tools/scope-registry.js';
import { TokenManager } from './token.js';
import { GoogleOAuthClient } from './oauth.js';
import logger from '../../utils/logger.js';
export class AccountManager {
private readonly accountsPath: string;
private accounts: Map<string, Account>;
private tokenManager!: TokenManager;
private oauthClient!: GoogleOAuthClient;
private currentAuthEmail?: string;
constructor(config?: AccountModuleConfig) {
// Use environment variable or config, fallback to Docker default
const defaultPath = process.env.ACCOUNTS_PATH ||
(process.env.MCP_MODE ? path.resolve(process.env.HOME || '', '.mcp/google-workspace-mcp/accounts.json') : '/app/config/accounts.json');
this.accountsPath = config?.accountsPath || defaultPath;
this.accounts = new Map();
}
async initialize(): Promise<void> {
logger.info('Initializing AccountManager...');
this.oauthClient = new GoogleOAuthClient();
this.tokenManager = new TokenManager(this.oauthClient);
// Set up automatic authentication completion
const { OAuthCallbackServer } = await import('./callback-server.js');
const callbackServer = OAuthCallbackServer.getInstance();
callbackServer.setAuthHandler(async (code: string) => {
if (this.currentAuthEmail) {
try {
logger.info(`Auto-completing authentication for ${this.currentAuthEmail}`);
const tokenData = await this.getTokenFromCode(code);
await this.saveToken(this.currentAuthEmail, tokenData);
logger.info(`Authentication completed automatically for ${this.currentAuthEmail}`);
this.currentAuthEmail = undefined;
} catch (error) {
logger.error('Failed to auto-complete authentication:', error);
this.currentAuthEmail = undefined;
}
}
});
await this.loadAccounts();
logger.info('AccountManager initialized successfully');
}
async listAccounts(): Promise<Account[]> {
logger.debug('Listing accounts with auth status');
const accounts = Array.from(this.accounts.values());
// Add auth status to each account and attempt auto-renewal if needed
for (const account of accounts) {
const renewalResult = await this.tokenManager.autoRenewToken(account.email);
if (renewalResult.success) {
account.auth_status = {
valid: true,
status: renewalResult.status
};
} else {
// If auto-renewal failed, try to get an auth URL for re-authentication
account.auth_status = {
valid: false,
status: renewalResult.status,
reason: renewalResult.reason,
authUrl: await this.generateAuthUrl()
};
}
}
logger.debug(`Found ${accounts.length} accounts`);
return accounts;
}
/**
* Wrapper for tool operations that handles token renewal
* @param email Account email
* @param operation Function that performs the actual operation
*/
async withTokenRenewal<T>(
email: string,
operation: () => Promise<T>
): Promise<T> {
try {
// Attempt auto-renewal before operation
const renewalResult = await this.tokenManager.autoRenewToken(email);
if (!renewalResult.success) {
if (renewalResult.canRetry) {
// If it's a temporary error, let the operation proceed
// The 401 handler below will catch and retry if needed
logger.warn('Token renewal failed but may be temporary - proceeding with operation');
} else {
// Only require re-auth if refresh token is invalid/revoked
throw new AccountError(
'Token renewal failed',
'TOKEN_RENEWAL_FAILED',
renewalResult.reason || 'Please re-authenticate your account'
);
}
}
// Perform the operation
return await operation();
} catch (error) {
if (error instanceof Error && 'code' in error && error.code === '401') {
// If we get a 401 during operation, try one more token renewal
logger.warn('Received 401 during operation, attempting final token renewal');
const finalRenewal = await this.tokenManager.autoRenewToken(email);
if (finalRenewal.success) {
// Retry the operation with renewed token
return await operation();
}
// Check if we should trigger full OAuth
if (!finalRenewal.canRetry) {
// Refresh token is invalid/revoked, need full reauth
throw new AccountError(
'Authentication failed',
'AUTH_REQUIRED',
finalRenewal.reason || 'Please re-authenticate your account'
);
} else {
// Temporary error, let caller handle retry
throw new AccountError(
'Token refresh failed temporarily',
'TEMPORARY_AUTH_ERROR',
'Please try again later'
);
}
}
throw error;
}
}
private validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
private async loadAccounts(): Promise<void> {
try {
logger.debug(`Loading accounts from ${this.accountsPath}`);
// Ensure directory exists
await fs.mkdir(path.dirname(this.accountsPath), { recursive: true });
let data: string;
try {
data = await fs.readFile(this.accountsPath, 'utf-8');
} catch (error) {
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
// Create empty accounts file if it doesn't exist
logger.info('Creating new accounts file');
data = JSON.stringify({ accounts: [] });
await fs.writeFile(this.accountsPath, data);
} else {
throw new AccountError(
'Failed to read accounts configuration',
'ACCOUNTS_READ_ERROR',
'Please ensure the accounts file is readable'
);
}
}
try {
const config = JSON.parse(data) as AccountsConfig;
this.accounts.clear();
for (const account of config.accounts) {
this.accounts.set(account.email, account);
}
} catch (error) {
throw new AccountError(
'Failed to parse accounts configuration',
'ACCOUNTS_PARSE_ERROR',
'Please ensure the accounts file contains valid JSON'
);
}
} catch (error) {
if (error instanceof AccountError) {
throw error;
}
throw new AccountError(
'Failed to load accounts configuration',
'ACCOUNTS_LOAD_ERROR',
'Please ensure accounts.json exists and is valid'
);
}
}
private async saveAccounts(): Promise<void> {
try {
const config: AccountsConfig = {
accounts: Array.from(this.accounts.values())
};
await fs.writeFile(
this.accountsPath,
JSON.stringify(config, null, 2)
);
} catch (error) {
throw new AccountError(
'Failed to save accounts configuration',
'ACCOUNTS_SAVE_ERROR',
'Please ensure accounts.json is writable'
);
}
}
async addAccount(email: string, category: string, description: string): Promise<Account> {
logger.info(`Adding new account: ${email}`);
if (!this.validateEmail(email)) {
logger.error(`Invalid email format: ${email}`);
throw new AccountError(
'Invalid email format',
'INVALID_EMAIL',
'Please provide a valid email address'
);
}
if (this.accounts.has(email)) {
throw new AccountError(
'Account already exists',
'DUPLICATE_ACCOUNT',
'Use updateAccount to modify existing accounts'
);
}
const account: Account = {
email,
category,
description
};
this.accounts.set(email, account);
await this.saveAccounts();
return account;
}
async updateAccount(email: string, updates: Partial<Omit<Account, 'email'>>): Promise<Account> {
const account = this.accounts.get(email);
if (!account) {
throw new AccountError(
'Account not found',
'ACCOUNT_NOT_FOUND',
'Please ensure the account exists before updating'
);
}
const updatedAccount: Account = {
...account,
...updates
};
this.accounts.set(email, updatedAccount);
await this.saveAccounts();
return updatedAccount;
}
async removeAccount(email: string): Promise<void> {
logger.info(`Removing account: ${email}`);
if (!this.accounts.has(email)) {
logger.error(`Account not found: ${email}`);
throw new AccountError(
'Account not found',
'ACCOUNT_NOT_FOUND',
'Cannot remove non-existent account'
);
}
// Delete token first
await this.tokenManager.deleteToken(email);
// Then remove account
this.accounts.delete(email);
await this.saveAccounts();
logger.info(`Successfully removed account: ${email}`);
}
async getAccount(email: string): Promise<Account | null> {
return this.accounts.get(email) || null;
}
async validateAccount(
email: string,
category?: string,
description?: string
): Promise<Account> {
logger.debug(`Validating account: ${email}`);
let account = await this.getAccount(email);
const isNewAccount: boolean = Boolean(!account && category && description);
try {
// Handle new account creation
if (isNewAccount && category && description) {
logger.info('Creating new account during validation');
account = await this.addAccount(email, category, description);
} else if (!account) {
throw new AccountError(
'Account not found',
'ACCOUNT_NOT_FOUND',
'Please provide category and description for new accounts'
);
}
// Validate token with appropriate flags for new accounts
const tokenStatus = await this.tokenManager.validateToken(email, isNewAccount);
// Map token status to account auth status
switch (tokenStatus.status) {
case 'NO_TOKEN':
account.auth_status = {
valid: false,
status: tokenStatus.status,
reason: isNewAccount ? 'New account requires authentication' : 'No token found',
authUrl: await this.generateAuthUrl()
};
break;
case 'VALID':
case 'REFRESHED':
account.auth_status = {
valid: true,
status: tokenStatus.status
};
break;
case 'INVALID':
case 'REFRESH_FAILED':
case 'EXPIRED':
account.auth_status = {
valid: false,
status: tokenStatus.status,
reason: tokenStatus.reason,
authUrl: await this.generateAuthUrl()
};
break;
case 'ERROR':
account.auth_status = {
valid: false,
status: tokenStatus.status,
reason: 'Authentication error occurred',
authUrl: await this.generateAuthUrl()
};
break;
}
logger.debug(`Account validation complete for ${email}. Status: ${tokenStatus.status}`);
return account;
} catch (error) {
logger.error('Account validation failed', error as Error);
if (error instanceof AccountError) {
throw error;
}
throw new AccountError(
'Account validation failed',
'VALIDATION_ERROR',
'An unexpected error occurred during account validation'
);
}
}
// OAuth related methods
async generateAuthUrl(): Promise<string> {
const allScopes = scopeRegistry.getAllScopes();
return this.oauthClient.generateAuthUrl(allScopes);
}
async startAuthentication(email: string): Promise<string> {
this.currentAuthEmail = email;
logger.info(`Starting authentication for ${email}`);
return this.generateAuthUrl();
}
async waitForAuthorizationCode(): Promise<string> {
return this.oauthClient.waitForAuthorizationCode();
}
async getTokenFromCode(code: string): Promise<any> {
const token = await this.oauthClient.getTokenFromCode(code);
return token;
}
async refreshToken(refreshToken: string): Promise<any> {
return this.oauthClient.refreshToken(refreshToken);
}
async getAuthClient() {
return this.oauthClient.getAuthClient();
}
// Token related methods
async validateToken(email: string, skipValidationForNew: boolean = false) {
return this.tokenManager.validateToken(email, skipValidationForNew);
}
async saveToken(email: string, tokenData: any) {
return this.tokenManager.saveToken(email, tokenData);
}
}
```
--------------------------------------------------------------------------------
/src/tools/type-guards.ts:
--------------------------------------------------------------------------------
```typescript
import {
BaseToolArguments,
CalendarEventParams,
SendEmailArgs,
ManageLabelParams,
ManageLabelAssignmentParams,
ManageLabelFilterParams,
ManageDraftParams,
DriveFileListArgs,
DriveSearchArgs,
DriveUploadArgs,
DriveDownloadArgs,
DriveFolderArgs,
DrivePermissionArgs,
DriveDeleteArgs,
ManageAttachmentParams
} from './types.js';
import { GetContactsParams } from '../modules/contacts/types.js';
// Base Tool Arguments
export function isBaseToolArguments(args: Record<string, unknown>): args is BaseToolArguments {
return typeof args.email === 'string';
}
export function assertBaseToolArguments(args: Record<string, unknown>): asserts args is BaseToolArguments {
if (!isBaseToolArguments(args)) {
throw new Error('Missing required email parameter');
}
}
// Calendar Type Guards
export function isCalendarEventParams(args: Record<string, unknown>): args is CalendarEventParams {
return typeof args.email === 'string' &&
(args.query === undefined || typeof args.query === 'string') &&
(args.maxResults === undefined || typeof args.maxResults === 'number') &&
(args.timeMin === undefined || typeof args.timeMin === 'string') &&
(args.timeMax === undefined || typeof args.timeMax === 'string');
}
export function assertCalendarEventParams(args: Record<string, unknown>): asserts args is CalendarEventParams {
if (!isCalendarEventParams(args)) {
throw new Error('Invalid calendar event parameters');
}
}
export function isEmailEventIdArgs(args: Record<string, unknown>): args is { email: string; eventId: string } {
return typeof args.email === 'string' && typeof args.eventId === 'string';
}
export function assertEmailEventIdArgs(args: Record<string, unknown>): asserts args is { email: string; eventId: string } {
if (!isEmailEventIdArgs(args)) {
throw new Error('Missing required email or eventId parameter');
}
}
// Gmail Type Guards
export function isSendEmailArgs(args: Record<string, unknown>): args is SendEmailArgs {
return typeof args.email === 'string' &&
Array.isArray(args.to) &&
args.to.every(to => typeof to === 'string') &&
typeof args.subject === 'string' &&
typeof args.body === 'string' &&
(args.cc === undefined || (Array.isArray(args.cc) && args.cc.every(cc => typeof cc === 'string'))) &&
(args.bcc === undefined || (Array.isArray(args.bcc) && args.bcc.every(bcc => typeof bcc === 'string')));
}
export function assertSendEmailArgs(args: Record<string, unknown>): asserts args is SendEmailArgs {
if (!isSendEmailArgs(args)) {
throw new Error('Invalid email parameters. Required: email, to, subject, body');
}
}
// Drive Type Guards
export function isDriveFileListArgs(args: unknown): args is DriveFileListArgs {
if (typeof args !== 'object' || args === null) return false;
const params = args as Partial<DriveFileListArgs>;
return typeof params.email === 'string' &&
(params.options === undefined || (() => {
const opts = params.options as any;
return (opts.folderId === undefined || typeof opts.folderId === 'string') &&
(opts.query === undefined || typeof opts.query === 'string') &&
(opts.pageSize === undefined || typeof opts.pageSize === 'number') &&
(opts.orderBy === undefined || (Array.isArray(opts.orderBy) && opts.orderBy.every((o: unknown) => typeof o === 'string'))) &&
(opts.fields === undefined || (Array.isArray(opts.fields) && opts.fields.every((f: unknown) => typeof f === 'string')));
})());
}
export function assertDriveFileListArgs(args: unknown): asserts args is DriveFileListArgs {
if (!isDriveFileListArgs(args)) {
throw new Error('Invalid file list parameters. Required: email');
}
}
export function isDriveSearchArgs(args: unknown): args is DriveSearchArgs {
if (typeof args !== 'object' || args === null) return false;
const params = args as Partial<DriveSearchArgs>;
return typeof params.email === 'string' &&
typeof params.options === 'object' && params.options !== null &&
(params.options.fullText === undefined || typeof params.options.fullText === 'string') &&
(params.options.mimeType === undefined || typeof params.options.mimeType === 'string') &&
(params.options.folderId === undefined || typeof params.options.folderId === 'string') &&
(params.options.trashed === undefined || typeof params.options.trashed === 'boolean') &&
(params.options.query === undefined || typeof params.options.query === 'string') &&
(params.options.pageSize === undefined || typeof params.options.pageSize === 'number');
}
export function assertDriveSearchArgs(args: unknown): asserts args is DriveSearchArgs {
if (!isDriveSearchArgs(args)) {
throw new Error('Invalid search parameters. Required: email, options');
}
}
export function isDriveUploadArgs(args: unknown): args is DriveUploadArgs {
if (typeof args !== 'object' || args === null) return false;
const params = args as Partial<DriveUploadArgs>;
return typeof params.email === 'string' &&
typeof params.options === 'object' && params.options !== null &&
typeof params.options.name === 'string' &&
typeof params.options.content === 'string' &&
(params.options.mimeType === undefined || typeof params.options.mimeType === 'string') &&
(params.options.parents === undefined || (Array.isArray(params.options.parents) && params.options.parents.every(p => typeof p === 'string')));
}
export function assertDriveUploadArgs(args: unknown): asserts args is DriveUploadArgs {
if (!isDriveUploadArgs(args)) {
throw new Error('Invalid upload parameters. Required: email, options.name, options.content');
}
}
export function isDriveDownloadArgs(args: unknown): args is DriveDownloadArgs {
if (typeof args !== 'object' || args === null) return false;
const params = args as Partial<DriveDownloadArgs>;
return typeof params.email === 'string' &&
typeof params.fileId === 'string' &&
(params.mimeType === undefined || typeof params.mimeType === 'string');
}
export function assertDriveDownloadArgs(args: unknown): asserts args is DriveDownloadArgs {
if (!isDriveDownloadArgs(args)) {
throw new Error('Invalid download parameters. Required: email, fileId');
}
}
export function isDriveFolderArgs(args: unknown): args is DriveFolderArgs {
if (typeof args !== 'object' || args === null) return false;
const params = args as Partial<DriveFolderArgs>;
return typeof params.email === 'string' &&
typeof params.name === 'string' &&
(params.parentId === undefined || typeof params.parentId === 'string');
}
export function assertDriveFolderArgs(args: unknown): asserts args is DriveFolderArgs {
if (!isDriveFolderArgs(args)) {
throw new Error('Invalid folder parameters. Required: email, name');
}
}
export function isDrivePermissionArgs(args: unknown): args is DrivePermissionArgs {
if (typeof args !== 'object' || args === null) return false;
const params = args as Partial<DrivePermissionArgs>;
return typeof params.email === 'string' &&
typeof params.options === 'object' && params.options !== null &&
typeof params.options.fileId === 'string' &&
['owner', 'organizer', 'fileOrganizer', 'writer', 'commenter', 'reader'].includes(params.options.role) &&
['user', 'group', 'domain', 'anyone'].includes(params.options.type) &&
(params.options.emailAddress === undefined || typeof params.options.emailAddress === 'string') &&
(params.options.domain === undefined || typeof params.options.domain === 'string') &&
(params.options.allowFileDiscovery === undefined || typeof params.options.allowFileDiscovery === 'boolean');
}
export function assertDrivePermissionArgs(args: unknown): asserts args is DrivePermissionArgs {
if (!isDrivePermissionArgs(args)) {
throw new Error('Invalid permission parameters. Required: email, options.fileId, options.role, options.type');
}
}
export function isDriveDeleteArgs(args: unknown): args is DriveDeleteArgs {
if (typeof args !== 'object' || args === null) return false;
const params = args as Partial<DriveDeleteArgs>;
return typeof params.email === 'string' &&
typeof params.fileId === 'string';
}
export function assertDriveDeleteArgs(args: unknown): asserts args is DriveDeleteArgs {
if (!isDriveDeleteArgs(args)) {
throw new Error('Invalid delete parameters. Required: email, fileId');
}
}
// Label Management Type Guards
export function isManageLabelParams(args: unknown): args is ManageLabelParams {
if (typeof args !== 'object' || args === null) return false;
const params = args as Partial<ManageLabelParams>;
return typeof params.email === 'string' &&
typeof params.action === 'string' &&
['create', 'read', 'update', 'delete'].includes(params.action) &&
(params.labelId === undefined || typeof params.labelId === 'string') &&
(params.data === undefined || (() => {
if (typeof params.data !== 'object' || params.data === null) return false;
const data = params.data as {
name?: string;
messageListVisibility?: string;
labelListVisibility?: string;
};
return (data.name === undefined || typeof data.name === 'string') &&
(data.messageListVisibility === undefined || ['show', 'hide'].includes(data.messageListVisibility)) &&
(data.labelListVisibility === undefined || ['labelShow', 'labelHide', 'labelShowIfUnread'].includes(data.labelListVisibility));
})());
}
export function assertManageLabelParams(args: unknown): asserts args is ManageLabelParams {
if (!isManageLabelParams(args)) {
throw new Error('Invalid label management parameters. Required: email, action');
}
}
export function isManageLabelAssignmentParams(args: unknown): args is ManageLabelAssignmentParams {
if (typeof args !== 'object' || args === null) return false;
const params = args as Partial<ManageLabelAssignmentParams>;
return typeof params.email === 'string' &&
typeof params.action === 'string' &&
['add', 'remove'].includes(params.action) &&
typeof params.messageId === 'string' &&
Array.isArray(params.labelIds) &&
params.labelIds.every((id: unknown) => typeof id === 'string');
}
export function assertManageLabelAssignmentParams(args: unknown): asserts args is ManageLabelAssignmentParams {
if (!isManageLabelAssignmentParams(args)) {
throw new Error('Invalid label assignment parameters. Required: email, action, messageId, labelIds');
}
}
export function isManageLabelFilterParams(args: unknown): args is ManageLabelFilterParams {
if (typeof args !== 'object' || args === null) return false;
const params = args as Partial<ManageLabelFilterParams>;
return typeof params.email === 'string' &&
typeof params.action === 'string' &&
['create', 'read', 'update', 'delete'].includes(params.action) &&
(params.filterId === undefined || typeof params.filterId === 'string') &&
(params.labelId === undefined || typeof params.labelId === 'string') &&
(params.data === undefined || (() => {
if (typeof params.data !== 'object' || params.data === null) return false;
const data = params.data as {
criteria?: { [key: string]: unknown };
actions?: { addLabel: boolean; markImportant?: boolean; markRead?: boolean; archive?: boolean };
};
return (data.criteria === undefined || (typeof data.criteria === 'object' && data.criteria !== null)) &&
(data.actions === undefined || (
typeof data.actions === 'object' &&
data.actions !== null &&
typeof data.actions.addLabel === 'boolean'
));
})());
}
export function assertManageLabelFilterParams(args: unknown): asserts args is ManageLabelFilterParams {
if (!isManageLabelFilterParams(args)) {
throw new Error('Invalid label filter parameters. Required: email, action');
}
}
// Draft Management Type Guards
export function isManageDraftParams(args: unknown): args is ManageDraftParams {
if (typeof args !== 'object' || args === null) return false;
const params = args as Partial<ManageDraftParams>;
return typeof params.email === 'string' &&
typeof params.action === 'string' &&
['create', 'read', 'update', 'delete', 'send'].includes(params.action) &&
(params.draftId === undefined || typeof params.draftId === 'string') &&
(params.data === undefined || (() => {
if (typeof params.data !== 'object' || params.data === null) return false;
const data = params.data as {
to?: string[];
subject?: string;
body?: string;
cc?: string[];
bcc?: string[];
replyToMessageId?: string;
threadId?: string;
references?: string[];
inReplyTo?: string;
};
return (data.to === undefined || (Array.isArray(data.to) && data.to.every(to => typeof to === 'string'))) &&
(data.subject === undefined || typeof data.subject === 'string') &&
(data.body === undefined || typeof data.body === 'string') &&
(data.cc === undefined || (Array.isArray(data.cc) && data.cc.every(cc => typeof cc === 'string'))) &&
(data.bcc === undefined || (Array.isArray(data.bcc) && data.bcc.every(bcc => typeof bcc === 'string'))) &&
(data.replyToMessageId === undefined || typeof data.replyToMessageId === 'string') &&
(data.threadId === undefined || typeof data.threadId === 'string') &&
(data.references === undefined || (Array.isArray(data.references) && data.references.every(ref => typeof ref === 'string'))) &&
(data.inReplyTo === undefined || typeof data.inReplyTo === 'string');
})());
}
export function isManageAttachmentParams(args: unknown): args is ManageAttachmentParams {
if (typeof args !== 'object' || args === null) return false;
const params = args as Partial<ManageAttachmentParams>;
return typeof params.email === 'string' &&
typeof params.action === 'string' &&
['download', 'upload', 'delete'].includes(params.action) &&
typeof params.source === 'string' &&
['email', 'calendar'].includes(params.source) &&
typeof params.messageId === 'string' &&
typeof params.filename === 'string' &&
(params.content === undefined || typeof params.content === 'string');
}
export function assertManageAttachmentParams(args: unknown): asserts args is ManageAttachmentParams {
if (!isManageAttachmentParams(args)) {
throw new Error('Invalid attachment management parameters. Required: email, action, source, messageId, filename');
}
}
export function assertManageDraftParams(args: unknown): asserts args is ManageDraftParams {
if (!isManageDraftParams(args)) {
throw new Error('Invalid draft management parameters. Required: email, action');
}
}
// Contacts Type Guards
export function isGetContactsParams(args: unknown): args is GetContactsParams {
if (typeof args !== 'object' || args === null) return false;
const params = args as Partial<GetContactsParams>;
return typeof params.email === 'string' &&
typeof params.personFields === 'string' &&
(params.pageSize === undefined || typeof params.pageSize === 'number') &&
(params.pageToken === undefined || typeof params.pageToken === 'string');
}
export function assertGetContactsParams(args: unknown): asserts args is GetContactsParams {
if (!isGetContactsParams(args)) {
throw new Error('Invalid contacts parameters. Required: email, personFields');
}
}
```
--------------------------------------------------------------------------------
/src/tools/gmail-handlers.ts:
--------------------------------------------------------------------------------
```typescript
import { getGmailService } from '../modules/gmail/index.js';
import { validateEmail } from '../utils/account.js';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { SendEmailParams } from '../modules/gmail/types.js';
import {
ManageLabelParams,
ManageLabelAssignmentParams,
ManageLabelFilterParams
} from '../modules/gmail/services/label.js';
import { getAccountManager } from '../modules/accounts/index.js';
import { AttachmentService } from '../modules/attachments/service.js';
import { ATTACHMENT_FOLDERS } from '../modules/attachments/types.js';
// Singleton instances
let gmailService: ReturnType<typeof getGmailService>;
let accountManager: ReturnType<typeof getAccountManager>;
let attachmentService: AttachmentService;
/**
* Initialize required services
*/
async function initializeServices() {
if (!gmailService) {
gmailService = getGmailService();
await gmailService.initialize();
}
if (!accountManager) {
accountManager = getAccountManager();
}
if (!attachmentService) {
attachmentService = AttachmentService.getInstance();
}
}
import {
GmailAttachment,
OutgoingGmailAttachment,
IncomingGmailAttachment
} from '../modules/gmail/types.js';
import { ManageAttachmentParams } from './types.js';
interface SearchEmailsParams {
email: string;
search?: {
from?: string | string[];
to?: string | string[];
subject?: string;
after?: string;
before?: string;
hasAttachment?: boolean;
labels?: string[];
excludeLabels?: string[];
includeSpam?: boolean;
isUnread?: boolean;
};
options?: {
maxResults?: number;
pageToken?: string;
format?: 'full' | 'metadata' | 'minimal';
includeHeaders?: boolean;
threadedView?: boolean;
sortOrder?: 'asc' | 'desc';
};
messageIds?: string[];
}
interface SendEmailRequestParams {
email: string;
to: string[];
subject: string;
body: string;
cc?: string[];
bcc?: string[];
attachments?: OutgoingGmailAttachment[];
}
interface ManageDraftParams {
email: string;
action: 'create' | 'read' | 'update' | 'delete' | 'send';
draftId?: string;
data?: {
to: string[];
subject: string;
body: string;
cc?: string[];
bcc?: string[];
attachments?: OutgoingGmailAttachment[];
};
}
export async function handleSearchWorkspaceEmails(params: SearchEmailsParams) {
await initializeServices();
const { email, search = {}, options = {}, messageIds } = params;
if (!email) {
throw new McpError(
ErrorCode.InvalidParams,
'Email address is required'
);
}
validateEmail(email);
return accountManager.withTokenRenewal(email, async () => {
try {
return await gmailService.getEmails({ email, search, options, messageIds });
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to search emails: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
});
}
export async function handleSendWorkspaceEmail(params: SendEmailRequestParams) {
await initializeServices();
const { email, to, subject, body, cc, bcc, attachments } = params;
if (!email) {
throw new McpError(
ErrorCode.InvalidParams,
'Sender email address is required'
);
}
if (!to || !Array.isArray(to) || to.length === 0) {
throw new McpError(
ErrorCode.InvalidParams,
'At least one recipient email address is required'
);
}
if (!subject) {
throw new McpError(
ErrorCode.InvalidParams,
'Email subject is required'
);
}
if (!body) {
throw new McpError(
ErrorCode.InvalidParams,
'Email body is required'
);
}
validateEmail(email);
to.forEach(validateEmail);
if (cc) cc.forEach(validateEmail);
if (bcc) bcc.forEach(validateEmail);
return accountManager.withTokenRenewal(email, async () => {
try {
const emailParams: SendEmailParams = {
email,
to,
subject,
body,
cc,
bcc,
attachments: attachments?.map(attachment => {
if (!attachment.content) {
throw new McpError(
ErrorCode.InvalidParams,
`Attachment content is required for file: ${attachment.name}`
);
}
return {
id: attachment.id,
name: attachment.name,
mimeType: attachment.mimeType,
size: attachment.size,
content: attachment.content
} as OutgoingGmailAttachment;
})
};
return await gmailService.sendEmail(emailParams);
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to send email: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
});
}
export async function handleGetWorkspaceGmailSettings(params: { email: string }) {
await initializeServices();
const { email } = params;
if (!email) {
throw new McpError(
ErrorCode.InvalidParams,
'Email address is required'
);
}
validateEmail(email);
return accountManager.withTokenRenewal(email, async () => {
try {
return await gmailService.getWorkspaceGmailSettings({ email });
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to get Gmail settings: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
});
}
export async function handleManageWorkspaceDraft(params: ManageDraftParams) {
await initializeServices();
const { email, action, draftId, data } = params;
if (!email) {
throw new McpError(
ErrorCode.InvalidParams,
'Email address is required'
);
}
if (!action) {
throw new McpError(
ErrorCode.InvalidParams,
'Action is required'
);
}
validateEmail(email);
return accountManager.withTokenRenewal(email, async () => {
try {
switch (action) {
case 'create':
if (!data) {
throw new McpError(
ErrorCode.InvalidParams,
'Draft data is required for create action'
);
}
return await gmailService.manageDraft({
email,
action: 'create',
data: {
...data,
attachments: data.attachments?.map(attachment => {
if (!attachment.content) {
throw new McpError(
ErrorCode.InvalidParams,
`Attachment content is required for file: ${attachment.name}`
);
}
return {
id: attachment.id,
name: attachment.name,
mimeType: attachment.mimeType,
size: attachment.size,
content: attachment.content
} as OutgoingGmailAttachment;
})
}
});
case 'read':
return await gmailService.manageDraft({
email,
action: 'read',
draftId
});
case 'update':
if (!draftId || !data) {
throw new McpError(
ErrorCode.InvalidParams,
'Draft ID and data are required for update action'
);
}
return await gmailService.manageDraft({
email,
action: 'update',
draftId,
data: {
...data,
attachments: data.attachments?.map(attachment => {
if (!attachment.content) {
throw new McpError(
ErrorCode.InvalidParams,
`Attachment content is required for file: ${attachment.name}`
);
}
return {
id: attachment.id,
name: attachment.name,
mimeType: attachment.mimeType,
size: attachment.size,
content: attachment.content
} as OutgoingGmailAttachment;
})
}
});
case 'delete':
if (!draftId) {
throw new McpError(
ErrorCode.InvalidParams,
'Draft ID is required for delete action'
);
}
return await gmailService.manageDraft({
email,
action: 'delete',
draftId
});
case 'send':
if (!draftId) {
throw new McpError(
ErrorCode.InvalidParams,
'Draft ID is required for send action'
);
}
return await gmailService.manageDraft({
email,
action: 'send',
draftId
});
default:
throw new McpError(
ErrorCode.InvalidParams,
'Invalid action. Supported actions are: create, read, update, delete, send'
);
}
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to manage draft: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
});
}
export async function handleManageWorkspaceLabel(params: ManageLabelParams) {
await initializeServices();
const { email } = params;
if (!email) {
throw new McpError(
ErrorCode.InvalidParams,
'Email address is required'
);
}
validateEmail(email);
return accountManager.withTokenRenewal(email, async () => {
try {
return await gmailService.manageLabel(params);
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to manage label: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
});
}
export async function handleManageWorkspaceLabelAssignment(params: ManageLabelAssignmentParams) {
await initializeServices();
const { email } = params;
if (!email) {
throw new McpError(
ErrorCode.InvalidParams,
'Email address is required'
);
}
validateEmail(email);
return accountManager.withTokenRenewal(email, async () => {
try {
await gmailService.manageLabelAssignment(params);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true })
}]
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to manage label assignment: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
});
}
export async function handleManageWorkspaceAttachment(params: ManageAttachmentParams) {
await initializeServices();
const { email, action, source, messageId, filename, content } = params;
// Validate all required parameters
if (!email || !action || !source || !messageId || !filename) {
throw new McpError(
ErrorCode.InvalidRequest,
'Invalid attachment management parameters. Required: email, action, source, messageId, filename'
);
}
validateEmail(email);
return accountManager.withTokenRenewal(email, async () => {
try {
// Get shared attachment service instance
if (!attachmentService) {
attachmentService = AttachmentService.getInstance();
await attachmentService.initialize(email);
}
// Determine parent folder based on source
const parentFolder = source === 'email' ?
ATTACHMENT_FOLDERS.EMAIL :
ATTACHMENT_FOLDERS.CALENDAR;
switch (action) {
case 'download': {
// First get the attachment data from Gmail
const gmailAttachment = await gmailService.getAttachment(email, messageId, filename);
if (!gmailAttachment || !gmailAttachment.content) {
throw new McpError(
ErrorCode.InvalidRequest,
'Attachment not found or content missing'
);
}
// Process and save the attachment locally
const result = await attachmentService.processAttachment(
email,
{
content: gmailAttachment.content,
metadata: {
name: gmailAttachment.name || `attachment_${Date.now()}`,
mimeType: gmailAttachment.mimeType || 'application/octet-stream',
size: gmailAttachment.size || 0
}
},
parentFolder
);
if (!result.success) {
throw new McpError(
ErrorCode.InternalError,
`Failed to save attachment: ${result.error}`
);
}
return {
success: true,
attachment: result.attachment
};
}
case 'upload': {
if (!content) {
throw new McpError(
ErrorCode.InvalidParams,
'File content is required for upload'
);
}
// Process attachment
const result = await attachmentService.processAttachment(
email,
{
content,
metadata: {
name: `attachment_${Date.now()}`, // Default name if not provided
mimeType: 'application/octet-stream', // Default type if not provided
}
},
parentFolder
);
if (!result.success) {
throw new McpError(
ErrorCode.InternalError,
`Failed to upload attachment: ${result.error}`
);
}
return {
success: true,
attachment: result.attachment
};
}
case 'delete': {
// Get the attachment metadata first to verify it exists
const gmailAttachment = await gmailService.getAttachment(email, messageId, filename);
if (!gmailAttachment || !gmailAttachment.path) {
throw new McpError(
ErrorCode.InvalidRequest,
'Attachment not found or path missing'
);
}
// Delete the attachment
const result = await attachmentService.deleteAttachment(
email,
gmailAttachment.id,
gmailAttachment.path
);
if (!result.success) {
throw new McpError(
ErrorCode.InternalError,
`Failed to delete attachment: ${result.error}`
);
}
return {
success: true,
attachment: result.attachment
};
}
default:
throw new McpError(
ErrorCode.InvalidParams,
'Invalid action. Supported actions are: download, upload, delete'
);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`Failed to manage attachment: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
});
}
export async function handleManageWorkspaceLabelFilter(params: ManageLabelFilterParams) {
await initializeServices();
const { email } = params;
if (!email) {
throw new McpError(
ErrorCode.InvalidParams,
'Email address is required'
);
}
validateEmail(email);
return accountManager.withTokenRenewal(email, async () => {
try {
return await gmailService.manageLabelFilter(params);
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to manage label filter: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
});
}
```
--------------------------------------------------------------------------------
/src/modules/calendar/service.ts:
--------------------------------------------------------------------------------
```typescript
import { google, calendar_v3 } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
import path from 'path';
import { getAccountManager } from '../accounts/index.js';
import {
GetEventsParams,
CreateEventParams,
EventResponse,
CreateEventResponse,
CalendarError,
CalendarModuleConfig,
ManageEventParams,
ManageEventResponse,
CalendarAttachment
} from './types.js';
import { AttachmentService } from '../attachments/service.js';
import { DriveService } from '../drive/service.js';
import { ATTACHMENT_FOLDERS, AttachmentSource, AttachmentMetadata } from '../attachments/types.js';
type CalendarEvent = calendar_v3.Schema$Event;
type GoogleEventAttachment = calendar_v3.Schema$EventAttachment;
// Convert Google Calendar attachment to our format
function convertToCalendarAttachment(attachment: GoogleEventAttachment): CalendarAttachment {
return {
content: attachment.fileUrl || '', // Use fileUrl as content for now
title: attachment.title || 'untitled',
mimeType: attachment.mimeType || 'application/octet-stream',
size: 0 // Size not available from Calendar API
};
}
// Convert our attachment format to Google Calendar format
function convertToGoogleAttachment(attachment: CalendarAttachment): GoogleEventAttachment {
return {
title: attachment.title,
mimeType: attachment.mimeType,
fileUrl: attachment.content // Store content in fileUrl
};
}
/**
* Google Calendar Service Implementation
*/
export class CalendarService {
private oauth2Client!: OAuth2Client;
private attachmentService?: AttachmentService;
private driveService?: DriveService;
private initialized = false;
private config?: CalendarModuleConfig;
constructor(config?: CalendarModuleConfig) {
this.config = config;
}
/**
* Initialize the Calendar service and all dependencies
*/
public async initialize(): Promise<void> {
try {
const accountManager = getAccountManager();
this.oauth2Client = await accountManager.getAuthClient();
this.driveService = new DriveService();
await this.driveService.ensureInitialized();
this.attachmentService = AttachmentService.getInstance({
maxSizeBytes: this.config?.maxAttachmentSize,
allowedMimeTypes: this.config?.allowedAttachmentTypes
});
this.initialized = true;
} catch (error) {
throw new CalendarError(
'Failed to initialize Calendar service',
'INIT_ERROR',
error instanceof Error ? error.message : 'Unknown error'
);
}
}
/**
* Ensure the Calendar service is initialized
*/
public async ensureInitialized(): Promise<void> {
if (!this.initialized) {
await this.initialize();
}
}
/**
* Check if the service is initialized
*/
private checkInitialized(): void {
if (!this.initialized) {
throw new CalendarError(
'Calendar service not initialized',
'INIT_ERROR',
'Please ensure the service is initialized before use'
);
}
}
/**
* Get an authenticated Google Calendar API client
*/
private async getCalendarClient(email: string) {
if (!this.initialized) {
await this.initialize();
}
const accountManager = getAccountManager();
try {
const tokenStatus = await accountManager.validateToken(email);
if (!tokenStatus.valid || !tokenStatus.token) {
throw new CalendarError(
'Calendar authentication required',
'AUTH_REQUIRED',
'Please authenticate to access calendar'
);
}
this.oauth2Client.setCredentials(tokenStatus.token);
return google.calendar({ version: 'v3', auth: this.oauth2Client });
} catch (error) {
if (error instanceof CalendarError) {
throw error;
}
throw new CalendarError(
'Failed to initialize Calendar client',
'AUTH_ERROR',
'Please try again or contact support if the issue persists'
);
}
}
/**
* Handle Calendar operations with automatic token refresh on 401/403
*/
private async handleCalendarOperation<T>(email: string, operation: () => Promise<T>): Promise<T> {
try {
return await operation();
} catch (error: any) {
if (error.code === 401 || error.code === 403) {
const accountManager = getAccountManager();
const tokenStatus = await accountManager.validateToken(email);
if (tokenStatus.valid && tokenStatus.token) {
this.oauth2Client.setCredentials(tokenStatus.token);
return await operation();
}
}
throw error;
}
}
/**
* Process event attachments directly like Gmail
*/
private async processEventAttachments(
email: string,
attachments: GoogleEventAttachment[]
): Promise<AttachmentMetadata[]> {
if (!this.attachmentService) {
throw new CalendarError(
'Calendar service not initialized',
'SERVICE_ERROR',
'Please ensure the service is initialized before processing attachments'
);
}
const processedAttachments: AttachmentMetadata[] = [];
for (const googleAttachment of attachments) {
// Convert Google attachment to our format
const attachment = convertToCalendarAttachment(googleAttachment);
const result = await this.attachmentService.processAttachment(
email,
{
content: attachment.content,
metadata: {
name: attachment.title,
mimeType: attachment.mimeType,
size: attachment.size
}
},
ATTACHMENT_FOLDERS.EVENT_FILES
);
if (result.success && result.attachment) {
processedAttachments.push(result.attachment);
}
}
return processedAttachments;
}
/**
* Map Calendar event to EventResponse
*/
private async mapEventResponse(email: string, event: CalendarEvent): Promise<EventResponse> {
// Process attachments if present
let attachments: { name: string }[] | undefined;
if (event.attachments && event.attachments.length > 0) {
// First process and store full metadata
const processedAttachments = await this.processEventAttachments(email, event.attachments);
// Then return simplified format for AI
attachments = processedAttachments.map(att => ({
name: att.name
}));
}
return {
id: event.id!,
summary: event.summary || '',
description: event.description || undefined,
start: {
dateTime: event.start?.dateTime || event.start?.date || '',
timeZone: event.start?.timeZone || 'UTC'
},
end: {
dateTime: event.end?.dateTime || event.end?.date || '',
timeZone: event.end?.timeZone || 'UTC'
},
attendees: event.attendees?.map(attendee => ({
email: attendee.email!,
responseStatus: attendee.responseStatus || undefined
})),
organizer: event.organizer ? {
email: event.organizer.email!,
self: event.organizer.self || false
} : undefined,
attachments: attachments?.length ? attachments : undefined
};
}
/**
* Retrieve calendar events with optional filtering
*/
async getEvents({ email, query, maxResults = 10, timeMin, timeMax }: GetEventsParams): Promise<EventResponse[]> {
const calendar = await this.getCalendarClient(email);
return this.handleCalendarOperation(email, async () => {
const params: calendar_v3.Params$Resource$Events$List = {
calendarId: 'primary',
maxResults,
singleEvents: true,
orderBy: 'startTime'
};
if (query) {
params.q = query;
}
if (timeMin) {
try {
const date = new Date(timeMin);
if (isNaN(date.getTime())) {
throw new Error('Invalid date');
}
params.timeMin = date.toISOString();
} catch (error) {
throw new CalendarError(
'Invalid date format',
'INVALID_DATE',
'Please provide dates in ISO format or YYYY-MM-DD format'
);
}
}
if (timeMax) {
try {
const date = new Date(timeMax);
if (isNaN(date.getTime())) {
throw new Error('Invalid date');
}
params.timeMax = date.toISOString();
} catch (error) {
throw new CalendarError(
'Invalid date format',
'INVALID_DATE',
'Please provide dates in ISO format or YYYY-MM-DD format'
);
}
}
const { data } = await calendar.events.list(params);
if (!data.items || data.items.length === 0) {
return [];
}
return Promise.all(data.items.map(event => this.mapEventResponse(email, event)));
});
}
/**
* Retrieve a single calendar event by ID
*/
async getEvent(email: string, eventId: string): Promise<EventResponse> {
const calendar = await this.getCalendarClient(email);
return this.handleCalendarOperation(email, async () => {
const { data: event } = await calendar.events.get({
calendarId: 'primary',
eventId
});
if (!event) {
throw new CalendarError(
'Event not found',
'NOT_FOUND',
`No event found with ID: ${eventId}`
);
}
return this.mapEventResponse(email, event);
});
}
/**
* Create a new calendar event
*/
async createEvent({ email, summary, description, start, end, attendees, attachments = [] }: CreateEventParams): Promise<CreateEventResponse> {
const calendar = await this.getCalendarClient(email);
return this.handleCalendarOperation(email, async () => {
// Process attachments first
const processedAttachments: GoogleEventAttachment[] = [];
for (const attachment of attachments) {
const source: AttachmentSource = {
content: attachment.content || '',
metadata: {
name: attachment.name,
mimeType: attachment.mimeType,
size: attachment.size
}
};
if (!this.attachmentService) {
throw new CalendarError(
'Calendar service not initialized',
'SERVICE_ERROR',
'Please ensure the service is initialized before processing attachments'
);
}
const result = await this.attachmentService.processAttachment(
email,
source,
ATTACHMENT_FOLDERS.EVENT_FILES
);
if (result.success && result.attachment) {
// Convert back to Google format
processedAttachments.push(convertToGoogleAttachment({
content: result.attachment.id, // Use ID as content
title: result.attachment.name,
mimeType: result.attachment.mimeType,
size: result.attachment.size
}));
}
}
const eventData: calendar_v3.Schema$Event = {
summary,
description,
start,
end,
attendees: attendees?.map(attendee => ({ email: attendee.email })),
attachments: processedAttachments.length > 0 ? processedAttachments : undefined
};
const { data: event } = await calendar.events.insert({
calendarId: 'primary',
requestBody: eventData,
sendUpdates: 'all'
});
if (!event.id || !event.summary) {
throw new CalendarError(
'Failed to create event',
'CREATE_ERROR',
'Event creation response was incomplete'
);
}
// Convert processed attachments to AttachmentMetadata format
const attachmentMetadata = processedAttachments.length > 0 ?
processedAttachments.map(a => ({
id: a.fileId!,
name: a.title!,
mimeType: a.mimeType!,
size: 0, // Size not available from Calendar API
path: path.join(this.attachmentService!.getAttachmentPath(ATTACHMENT_FOLDERS.EVENT_FILES), `${a.fileId}_${a.title}`)
})) :
undefined;
return {
id: event.id,
summary: event.summary,
htmlLink: event.htmlLink || '',
attachments: attachmentMetadata
};
});
}
/**
* Manage calendar event responses and updates
*/
async manageEvent({ email, eventId, action, comment, newTimes }: ManageEventParams): Promise<ManageEventResponse> {
const calendar = await this.getCalendarClient(email);
return this.handleCalendarOperation(email, async () => {
const event = await calendar.events.get({
calendarId: 'primary',
eventId
});
if (!event.data) {
throw new CalendarError(
'Event not found',
'NOT_FOUND',
`No event found with ID: ${eventId}`
);
}
switch (action) {
case 'accept':
case 'decline':
case 'tentative': {
const responseStatus = action === 'accept' ? 'accepted' :
action === 'decline' ? 'declined' :
'tentative';
const { data: updatedEvent } = await calendar.events.patch({
calendarId: 'primary',
eventId,
sendUpdates: 'all',
requestBody: {
attendees: [
{
email,
responseStatus
}
]
}
});
return {
success: true,
eventId,
action,
status: 'completed',
htmlLink: updatedEvent.htmlLink || undefined
};
}
case 'propose_new_time': {
if (!newTimes || newTimes.length === 0) {
throw new CalendarError(
'No proposed times provided',
'INVALID_REQUEST',
'Please provide at least one proposed time'
);
}
const counterProposal = await calendar.events.insert({
calendarId: 'primary',
requestBody: {
summary: `Counter-proposal: ${event.data.summary}`,
description: `Counter-proposal for original event.\n\nComment: ${comment || 'No comment provided'}\n\nOriginal event: ${event.data.htmlLink}`,
start: newTimes[0].start,
end: newTimes[0].end,
attendees: event.data.attendees
}
});
return {
success: true,
eventId,
action,
status: 'proposed',
htmlLink: counterProposal.data.htmlLink || undefined,
proposedTimes: newTimes.map(time => ({
start: { dateTime: time.start.dateTime, timeZone: time.start.timeZone || 'UTC' },
end: { dateTime: time.end.dateTime, timeZone: time.end.timeZone || 'UTC' }
}))
};
}
case 'update_time': {
if (!newTimes || newTimes.length === 0) {
throw new CalendarError(
'No new time provided',
'INVALID_REQUEST',
'Please provide the new time for the event'
);
}
const { data: updatedEvent } = await calendar.events.patch({
calendarId: 'primary',
eventId,
requestBody: {
start: newTimes[0].start,
end: newTimes[0].end
},
sendUpdates: 'all'
});
return {
success: true,
eventId,
action,
status: 'updated',
htmlLink: updatedEvent.htmlLink || undefined
};
}
default:
throw new CalendarError(
'Supported actions are: accept, decline, tentative, propose_new_time, update_time',
'INVALID_ACTION',
'Invalid action'
);
}
});
}
/**
* Delete a calendar event
*
* @param email User's email address
* @param eventId ID of the event to delete
* @param sendUpdates Whether to send update notifications
* @param deletionScope For recurring events, specifies which instances to delete
*/
async deleteEvent(
email: string,
eventId: string,
sendUpdates: 'all' | 'externalOnly' | 'none' = 'all',
deletionScope?: 'entire_series' | 'this_and_following'
): Promise<void> {
const calendar = await this.getCalendarClient(email);
return this.handleCalendarOperation(email, async () => {
// If no deletion scope is specified or it's set to delete entire series,
// use the default behavior
if (!deletionScope || deletionScope === 'entire_series') {
await calendar.events.delete({
calendarId: 'primary',
eventId,
sendUpdates
});
return;
}
// For 'this_and_following', we need to check if this is a recurring event
try {
const { data: event } = await calendar.events.get({
calendarId: 'primary',
eventId
});
const isRecurring = !!event.recurringEventId || !!event.recurrence;
if (!isRecurring) {
throw new CalendarError(
'Deletion scope can only be applied to recurring events',
'INVALID_REQUEST',
'The specified event is not recurring'
);
}
// For 'this_and_following', we need to use a different approach
// Google Calendar API handles recurring events differently
if (event.recurringEventId) {
// This is an instance of a recurring event
// We need to get the master event to update its recurrence rules
const { data: masterEvent } = await calendar.events.get({
calendarId: 'primary',
eventId: event.recurringEventId
});
if (!masterEvent || !masterEvent.recurrence) {
throw new CalendarError(
'Failed to retrieve master event',
'NOT_FOUND',
'Could not find the master event for this recurring instance'
);
}
// Get the instance date to use as the cutoff
const instanceDate = new Date(event.start?.dateTime || event.start?.date || '');
// Format the date as YYYYMMDD for UNTIL parameter in RRULE
// Subtract one day to make it exclusive (end before this instance)
instanceDate.setDate(instanceDate.getDate() - 1);
const formattedDate = instanceDate.toISOString().slice(0, 10).replace(/-/g, '');
// Update the recurrence rules to end before this instance
const updatedRules = masterEvent.recurrence.map(rule => {
if (rule.startsWith('RRULE:')) {
// If the rule already has an UNTIL parameter, we need to replace it
if (rule.includes('UNTIL=')) {
return rule.replace(/UNTIL=\d+T\d+Z?/, `UNTIL=${formattedDate}`);
} else {
// Otherwise, add the UNTIL parameter
return `${rule};UNTIL=${formattedDate}`;
}
}
return rule;
});
// Update the master event with the new recurrence rules
await calendar.events.patch({
calendarId: 'primary',
eventId: event.recurringEventId,
sendUpdates,
requestBody: {
recurrence: updatedRules
}
});
// Now delete this specific instance
await calendar.events.delete({
calendarId: 'primary',
eventId,
sendUpdates
});
} else if (event.recurrence) {
// This is a master event with recurrence rules
// We need to update the recurrence rule to end before this instance
const eventDate = new Date(event.start?.dateTime || event.start?.date || '');
// Format the date as YYYYMMDD for UNTIL parameter in RRULE
const formattedDate = eventDate.toISOString().slice(0, 10).replace(/-/g, '');
// Get the existing recurrence rules
const recurrenceRules = event.recurrence || [];
// Update the RRULE to include UNTIL parameter
const updatedRules = recurrenceRules.map(rule => {
if (rule.startsWith('RRULE:')) {
// If the rule already has an UNTIL parameter, we need to replace it
if (rule.includes('UNTIL=')) {
return rule.replace(/UNTIL=\d+T\d+Z?/, `UNTIL=${formattedDate}`);
} else {
// Otherwise, add the UNTIL parameter
return `${rule};UNTIL=${formattedDate}`;
}
}
return rule;
});
// Update the master event with the new recurrence rules
await calendar.events.patch({
calendarId: 'primary',
eventId,
sendUpdates,
requestBody: {
recurrence: updatedRules
}
});
}
} catch (error) {
if (error instanceof CalendarError) {
throw error;
}
// If we can't get the event or another error occurs, fall back to simple delete
await calendar.events.delete({
calendarId: 'primary',
eventId,
sendUpdates
});
}
});
}
}
```
--------------------------------------------------------------------------------
/src/modules/gmail/services/label.ts:
--------------------------------------------------------------------------------
```typescript
import { gmail_v1 } from 'googleapis';
import {
Label,
CreateLabelParams,
UpdateLabelParams,
DeleteLabelParams,
GetLabelsParams,
GetLabelsResponse,
ModifyMessageLabelsParams,
GmailError,
CreateLabelFilterParams,
GetLabelFiltersParams,
GetLabelFiltersResponse,
UpdateLabelFilterParams,
DeleteLabelFilterParams,
LabelFilterCriteria,
LabelFilterActions,
LabelFilter
} from '../types.js';
import {
isValidGmailLabelColor,
getNearestGmailLabelColor,
LABEL_ERROR_MESSAGES
} from '../constants.js';
export type LabelAction = 'create' | 'read' | 'update' | 'delete';
export type LabelAssignmentAction = 'add' | 'remove';
export type LabelFilterAction = 'create' | 'read' | 'update' | 'delete';
export interface ManageLabelParams {
action: LabelAction;
email: string;
labelId?: string;
data?: {
name?: string;
messageListVisibility?: 'show' | 'hide';
labelListVisibility?: 'labelShow' | 'labelHide' | 'labelShowIfUnread';
color?: {
textColor: string;
backgroundColor: string;
};
};
}
export interface ManageLabelAssignmentParams {
action: LabelAssignmentAction;
email: string;
messageId: string;
labelIds: string[];
}
export interface ManageLabelFilterParams {
action: LabelFilterAction;
email: string;
filterId?: string;
labelId?: string;
data?: {
criteria: LabelFilterCriteria;
actions: LabelFilterActions;
};
}
export class LabelService {
private client: gmail_v1.Gmail | null = null;
updateClient(client: gmail_v1.Gmail) {
this.client = client;
}
private ensureClient() {
if (!this.client) {
throw new GmailError(
'Gmail client not initialized',
'CLIENT_ERROR',
'Please ensure the service is initialized with a valid client'
);
}
}
async manageLabel(params: ManageLabelParams): Promise<Label | GetLabelsResponse | void> {
this.ensureClient();
switch (params.action) {
case 'create':
if (!params.data?.name) {
throw new GmailError(
'Label name is required for creation',
'VALIDATION_ERROR',
'Please provide a name for the label'
);
}
return this.createLabel({
email: params.email,
name: params.data.name,
messageListVisibility: params.data.messageListVisibility,
labelListVisibility: params.data.labelListVisibility,
color: params.data.color
});
case 'read':
if (params.labelId) {
// Get specific label
const response = await this.client?.users.labels.get({
userId: params.email,
id: params.labelId
});
if (!response?.data) {
throw new GmailError(
'Label not found',
'NOT_FOUND_ERROR',
`Label ${params.labelId} does not exist`
);
}
return this.mapGmailLabel(response.data);
} else {
// Get all labels
return this.getLabels({ email: params.email });
}
case 'update':
if (!params.labelId) {
throw new GmailError(
'Label ID is required for update',
'VALIDATION_ERROR',
'Please provide a label ID'
);
}
return this.updateLabel({
email: params.email,
labelId: params.labelId,
...params.data
});
case 'delete':
if (!params.labelId) {
throw new GmailError(
'Label ID is required for deletion',
'VALIDATION_ERROR',
'Please provide a label ID'
);
}
return this.deleteLabel({
email: params.email,
labelId: params.labelId
});
default:
throw new GmailError(
'Invalid label action',
'VALIDATION_ERROR',
`Action ${params.action} is not supported`
);
}
}
async manageLabelAssignment(params: ManageLabelAssignmentParams): Promise<void> {
this.ensureClient();
const modifyParams: ModifyMessageLabelsParams = {
email: params.email,
messageId: params.messageId,
addLabelIds: params.action === 'add' ? params.labelIds : [],
removeLabelIds: params.action === 'remove' ? params.labelIds : []
};
return this.modifyMessageLabels(modifyParams);
}
/**
* Validate filter criteria to ensure all required fields are present and properly formatted
*/
private validateFilterCriteria(criteria: LabelFilterCriteria): void {
if (!criteria) {
throw new GmailError(
'Filter criteria is required',
'VALIDATION_ERROR',
'Please provide filter criteria'
);
}
// At least one filtering condition must be specified
const hasCondition = (criteria.from && criteria.from.length > 0) ||
(criteria.to && criteria.to.length > 0) ||
criteria.subject ||
(criteria.hasWords && criteria.hasWords.length > 0) ||
(criteria.doesNotHaveWords && criteria.doesNotHaveWords.length > 0) ||
criteria.hasAttachment ||
criteria.size;
if (!hasCondition) {
throw new GmailError(
'Invalid filter criteria',
'VALIDATION_ERROR',
'At least one filtering condition must be specified (from, to, subject, hasWords, doesNotHaveWords, hasAttachment, or size)'
);
}
// Validate email arrays
if (criteria.from?.length) {
criteria.from.forEach(email => {
if (!email.includes('@')) {
throw new GmailError(
'Invalid email address in from criteria',
'VALIDATION_ERROR',
`Invalid email address: ${email}`
);
}
});
}
if (criteria.to?.length) {
criteria.to.forEach(email => {
if (!email.includes('@')) {
throw new GmailError(
'Invalid email address in to criteria',
'VALIDATION_ERROR',
`Invalid email address: ${email}`
);
}
});
}
// Validate size criteria if present
if (criteria.size) {
if (typeof criteria.size.size !== 'number' || criteria.size.size <= 0) {
throw new GmailError(
'Invalid size criteria',
'VALIDATION_ERROR',
'Size must be a positive number'
);
}
if (!['larger', 'smaller'].includes(criteria.size.operator)) {
throw new GmailError(
'Invalid size operator',
'VALIDATION_ERROR',
'Size operator must be either "larger" or "smaller"'
);
}
}
}
/**
* Build Gmail API query string from filter criteria
*/
private buildFilterQuery(criteria: LabelFilterCriteria): string {
const conditions: string[] = [];
if (criteria.from?.length) {
conditions.push(`{${criteria.from.map(email => `from:${email}`).join(' OR ')}}`);
}
if (criteria.to?.length) {
conditions.push(`{${criteria.to.map(email => `to:${email}`).join(' OR ')}}`);
}
if (criteria.subject) {
conditions.push(`subject:"${criteria.subject}"`);
}
if (criteria.hasWords?.length) {
conditions.push(`{${criteria.hasWords.join(' OR ')}}`);
}
if (criteria.doesNotHaveWords?.length) {
conditions.push(`-{${criteria.doesNotHaveWords.join(' OR ')}}`);
}
if (criteria.hasAttachment) {
conditions.push('has:attachment');
}
if (criteria.size) {
conditions.push(`size${criteria.size.operator === 'larger' ? '>' : '<'}${criteria.size.size}`);
}
return conditions.join(' ');
}
async manageLabelFilter(params: ManageLabelFilterParams): Promise<LabelFilter | GetLabelFiltersResponse | void> {
this.ensureClient();
switch (params.action) {
case 'create':
if (!params.labelId) {
throw new GmailError(
'Label ID is required',
'VALIDATION_ERROR',
'Please provide a valid label ID'
);
}
if (!params.data?.criteria || !params.data?.actions) {
throw new GmailError(
'Filter configuration is required',
'VALIDATION_ERROR',
'Please provide both criteria and actions for the filter'
);
}
// Validate filter criteria
this.validateFilterCriteria(params.data.criteria);
return this.createLabelFilter({
email: params.email,
labelId: params.labelId,
criteria: params.data.criteria,
actions: params.data.actions
});
case 'read':
return this.getLabelFilters({
email: params.email,
labelId: params.labelId
});
case 'update':
if (!params.filterId || !params.labelId || !params.data?.criteria || !params.data?.actions) {
throw new GmailError(
'Missing required filter update data',
'VALIDATION_ERROR',
'Please provide filterId, labelId, criteria, and actions'
);
}
return this.updateLabelFilter({
email: params.email,
filterId: params.filterId,
labelId: params.labelId,
criteria: params.data.criteria,
actions: params.data.actions
});
case 'delete':
if (!params.filterId) {
throw new GmailError(
'Filter ID is required for deletion',
'VALIDATION_ERROR',
'Please provide a filter ID'
);
}
return this.deleteLabelFilter({
email: params.email,
filterId: params.filterId
});
default:
throw new GmailError(
'Invalid filter action',
'VALIDATION_ERROR',
`Action ${params.action} is not supported`
);
}
}
// Helper methods that implement the actual operations
private async createLabel(params: CreateLabelParams): Promise<Label> {
try {
if (params.color) {
const { textColor, backgroundColor } = params.color;
if (!isValidGmailLabelColor(textColor, backgroundColor)) {
const suggestedColor = getNearestGmailLabelColor(backgroundColor);
throw new GmailError(
LABEL_ERROR_MESSAGES.INVALID_COLOR,
'COLOR_ERROR',
LABEL_ERROR_MESSAGES.COLOR_SUGGESTION(backgroundColor, suggestedColor)
);
}
}
if (!this.client) {
throw new GmailError(
'Gmail client not initialized',
'CLIENT_ERROR',
'Please ensure the service is initialized with a valid client'
);
}
const response = await this.client.users.labels.create({
userId: params.email,
requestBody: {
name: params.name,
messageListVisibility: params.messageListVisibility || 'show',
labelListVisibility: params.labelListVisibility || 'labelShow',
color: params.color && {
textColor: params.color.textColor,
backgroundColor: params.color.backgroundColor
}
}
});
if (!response?.data) {
throw new GmailError(
'No response data from create label request',
'CREATE_ERROR',
'Server returned empty response'
);
}
return this.mapGmailLabel(response.data);
} catch (error: unknown) {
if (error instanceof Error && 'code' in error && error.code === '401') {
throw new GmailError(
'Authentication failed',
'AUTH_ERROR',
'Please re-authenticate your account'
);
}
if (error instanceof Error && error.message.includes('Invalid grant')) {
throw new GmailError(
'Authentication token expired',
'TOKEN_ERROR',
'Please re-authenticate your account'
);
}
throw new GmailError(
'Failed to create label',
'CREATE_ERROR',
error instanceof Error ? error.message : 'Unknown error'
);
}
}
private async getLabels(params: GetLabelsParams): Promise<GetLabelsResponse> {
try {
const response = await this.client?.users.labels.list({
userId: params.email
});
if (!response?.data.labels) {
return { labels: [] };
}
return {
labels: response.data.labels.map(this.mapGmailLabel)
};
} catch (error: unknown) {
throw new GmailError(
'Failed to fetch labels',
'FETCH_ERROR',
error instanceof Error ? error.message : 'Unknown error'
);
}
}
private async updateLabel(params: UpdateLabelParams): Promise<Label> {
try {
if (params.color) {
const { textColor, backgroundColor } = params.color;
if (!isValidGmailLabelColor(textColor, backgroundColor)) {
const suggestedColor = getNearestGmailLabelColor(backgroundColor);
throw new GmailError(
LABEL_ERROR_MESSAGES.INVALID_COLOR,
'COLOR_ERROR',
LABEL_ERROR_MESSAGES.COLOR_SUGGESTION(backgroundColor, suggestedColor)
);
}
}
if (!this.client) {
throw new GmailError(
'Gmail client not initialized',
'CLIENT_ERROR',
'Please ensure the service is initialized with a valid client'
);
}
const response = await this.client.users.labels.patch({
userId: params.email,
id: params.labelId,
requestBody: {
name: params.name,
messageListVisibility: params.messageListVisibility,
labelListVisibility: params.labelListVisibility,
color: params.color && {
textColor: params.color.textColor,
backgroundColor: params.color.backgroundColor
}
}
});
if (!response?.data) {
throw new GmailError(
'No response data from update label request',
'UPDATE_ERROR',
'Server returned empty response'
);
}
return this.mapGmailLabel(response.data);
} catch (error: unknown) {
if (error instanceof Error && 'code' in error && error.code === '401') {
throw new GmailError(
'Authentication failed',
'AUTH_ERROR',
'Please re-authenticate your account'
);
}
if (error instanceof Error && error.message.includes('Invalid grant')) {
throw new GmailError(
'Authentication token expired',
'TOKEN_ERROR',
'Please re-authenticate your account'
);
}
throw new GmailError(
'Failed to update label',
'UPDATE_ERROR',
error instanceof Error ? error.message : 'Unknown error'
);
}
}
private async deleteLabel(params: DeleteLabelParams): Promise<void> {
try {
await this.client?.users.labels.delete({
userId: params.email,
id: params.labelId
});
} catch (error: unknown) {
throw new GmailError(
'Failed to delete label',
'DELETE_ERROR',
error instanceof Error ? error.message : 'Unknown error'
);
}
}
private async modifyMessageLabels(params: ModifyMessageLabelsParams): Promise<void> {
try {
await this.client?.users.messages.modify({
userId: params.email,
id: params.messageId,
requestBody: {
addLabelIds: params.addLabelIds,
removeLabelIds: params.removeLabelIds
}
});
} catch (error: unknown) {
throw new GmailError(
'Failed to modify message labels',
'MODIFY_ERROR',
error instanceof Error ? error.message : 'Unknown error'
);
}
}
private async createLabelFilter(params: CreateLabelFilterParams): Promise<LabelFilter> {
try {
// Build filter criteria for Gmail API
const filterCriteria: gmail_v1.Schema$FilterCriteria = {
from: params.criteria.from?.join(' OR ') || undefined,
to: params.criteria.to?.join(' OR ') || undefined,
subject: params.criteria.subject || undefined,
query: this.buildFilterQuery(params.criteria),
hasAttachment: params.criteria.hasAttachment || undefined,
excludeChats: true,
size: params.criteria.size ? Number(params.criteria.size.size) : undefined,
sizeComparison: params.criteria.size?.operator || undefined
};
// Build filter action with initialized arrays
const addLabelIds = [params.labelId];
const removeLabelIds: string[] = [];
// Add system label modifications based on actions
if (params.actions.markImportant) {
addLabelIds.push('IMPORTANT');
}
if (params.actions.markRead) {
removeLabelIds.push('UNREAD');
}
if (params.actions.archive) {
removeLabelIds.push('INBOX');
}
const filterAction: gmail_v1.Schema$FilterAction = {
addLabelIds,
removeLabelIds,
forward: undefined
};
// Create the filter
const response = await this.client?.users.settings.filters.create({
userId: params.email,
requestBody: {
criteria: filterCriteria,
action: filterAction
}
});
if (!response?.data) {
throw new GmailError(
'Failed to create filter',
'CREATE_ERROR',
'No response data received from Gmail API'
);
}
// Return the created filter in our standard format
return {
id: response.data.id || '',
labelId: params.labelId,
criteria: params.criteria,
actions: params.actions
};
} catch (error: unknown) {
throw new GmailError(
'Failed to create label filter',
'CREATE_ERROR',
error instanceof Error ? error.message : 'Unknown error'
);
}
}
private async getLabelFilters(params: GetLabelFiltersParams): Promise<GetLabelFiltersResponse> {
try {
const response = await this.client?.users.settings.filters.list({
userId: params.email
});
if (!response?.data.filter) {
return { filters: [] };
}
// Map Gmail API filters to our format
const filters = response.data.filter
.filter(filter => {
if (!filter.action?.addLabelIds?.length) return false;
// If labelId is provided, only return filters for that label
if (params.labelId) {
return filter.action.addLabelIds.includes(params.labelId);
}
return true;
})
.map(filter => ({
id: filter.id || '',
labelId: filter.action?.addLabelIds?.[0] || '',
criteria: {
from: filter.criteria?.from ? filter.criteria.from.split(' OR ') : undefined,
to: filter.criteria?.to ? filter.criteria.to.split(' OR ') : undefined,
subject: filter.criteria?.subject || undefined,
hasAttachment: filter.criteria?.hasAttachment || undefined,
hasWords: filter.criteria?.query ? [filter.criteria.query] : undefined,
doesNotHaveWords: filter.criteria?.negatedQuery ? [filter.criteria.negatedQuery] : undefined,
size: filter.criteria?.size && filter.criteria?.sizeComparison ? {
operator: filter.criteria.sizeComparison as 'larger' | 'smaller',
size: Number(filter.criteria.size)
} : undefined
},
actions: {
addLabel: true,
markImportant: filter.action?.addLabelIds?.includes('IMPORTANT') || false,
markRead: filter.action?.removeLabelIds?.includes('UNREAD') || false,
archive: filter.action?.removeLabelIds?.includes('INBOX') || false
}
}));
return { filters };
} catch (error: unknown) {
throw new GmailError(
'Failed to get label filters',
'FETCH_ERROR',
error instanceof Error ? error.message : 'Unknown error'
);
}
}
private async updateLabelFilter(params: UpdateLabelFilterParams): Promise<LabelFilter> {
try {
// Gmail API doesn't support direct filter updates, so we need to delete and recreate
await this.deleteLabelFilter({
email: params.email,
filterId: params.filterId
});
// Convert our criteria format to Gmail API format
const criteria: gmail_v1.Schema$FilterCriteria = {
from: params.criteria.from?.join(' OR ') || null,
to: params.criteria.to?.join(' OR ') || null,
subject: params.criteria.subject || null,
query: params.criteria.hasWords?.join(' OR ') || null,
negatedQuery: params.criteria.doesNotHaveWords?.join(' OR ') || null,
hasAttachment: params.criteria.hasAttachment || null,
size: params.criteria.size?.size || null,
sizeComparison: params.criteria.size?.operator || null
};
// Initialize arrays for label IDs
const addLabelIds: string[] = [params.labelId];
const removeLabelIds: string[] = [];
// Add additional label IDs based on actions
if (params.actions.markImportant) {
addLabelIds.push('IMPORTANT');
}
if (params.actions.markRead) {
removeLabelIds.push('UNREAD');
}
if (params.actions.archive) {
removeLabelIds.push('INBOX');
}
// Create the filter action
const action: gmail_v1.Schema$FilterAction = {
addLabelIds,
removeLabelIds,
forward: null
};
const response = await this.client?.users.settings.filters.create({
userId: params.email,
requestBody: {
criteria,
action
}
});
if (!response?.data) {
throw new GmailError(
'No response data from update filter request',
'UPDATE_ERROR',
'Server returned empty response'
);
}
// Map response to our LabelFilter format
return {
id: response.data.id || '',
labelId: params.labelId,
criteria: params.criteria,
actions: params.actions
};
} catch (error: unknown) {
throw new GmailError(
'Failed to update label filter',
'UPDATE_ERROR',
error instanceof Error ? error.message : 'Unknown error'
);
}
}
private async deleteLabelFilter(params: DeleteLabelFilterParams): Promise<void> {
try {
await this.client?.users.settings.filters.delete({
userId: params.email,
id: params.filterId
});
} catch (error: unknown) {
throw new GmailError(
'Failed to delete label filter',
'DELETE_ERROR',
error instanceof Error ? error.message : 'Unknown error'
);
}
}
private mapGmailLabel(label: gmail_v1.Schema$Label): Label {
const mappedLabel: Label = {
id: label.id || '',
name: label.name || '',
type: (label.type || 'user') as 'system' | 'user',
messageListVisibility: (label.messageListVisibility || 'show') as 'hide' | 'show',
labelListVisibility: (label.labelListVisibility || 'labelShow') as 'labelHide' | 'labelShow' | 'labelShowIfUnread'
};
if (label.color?.textColor && label.color?.backgroundColor) {
mappedLabel.color = {
textColor: label.color.textColor,
backgroundColor: label.color.backgroundColor
};
}
return mappedLabel;
}
}
```
--------------------------------------------------------------------------------
/src/tools/definitions.ts:
--------------------------------------------------------------------------------
```typescript
import { ToolMetadata } from "../modules/tools/registry.js";
// Account Management Tools
export const accountTools: ToolMetadata[] = [
{
name: 'list_workspace_accounts',
category: 'Account Management',
description: `List all configured Google workspace accounts and their authentication status.
IMPORTANT: This tool MUST be called first before any other workspace operations to:
1. Check for existing authenticated accounts
2. Determine which account to use if multiple exist
3. Verify required API scopes are authorized
Common Response Patterns:
- Valid account exists → Proceed with requested operation
- Multiple accounts exist → Ask user which to use
- Token expired → Proceed normally (auto-refresh occurs)
- No accounts exist → Start authentication flow
Example Usage:
1. User asks to "check email"
2. Call this tool first to validate account access
3. If account valid, proceed to email operations
4. If multiple accounts, ask user "Which account would you like to use?"
5. Remember chosen account for subsequent operations`,
aliases: ['list_accounts', 'get_accounts', 'show_accounts'],
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'authenticate_workspace_account',
category: 'Account Management',
description: `Add and authenticate a Google account for API access.
IMPORTANT: Only use this tool if list_workspace_accounts shows:
1. No existing accounts, OR
2. When using the account it seems to lack necessary auth scopes.
To prevent wasted time, DO NOT use this tool:
- Without checking list_workspace_accounts first
- When token is just expired (auto-refresh handles this)
- To re-authenticate an already valid account
Steps to complete authentication:
1. You call with required email address
2. You receive auth_url in response
3. You share EXACT auth_url with user - in a clickable URL form! (Important!)
4. User completes OAuth flow by clicking on the link you furnished them
5. User provides auth_code back to you
6. Complete authentication with auth_code`,
aliases: ['auth_account', 'add_account', 'connect_account'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Google account to authenticate'
},
category: {
type: 'string',
description: 'Account category (e.g., work, personal)'
},
description: {
type: 'string',
description: 'Account description'
},
auth_code: {
type: 'string',
description: 'Authorization code from Google OAuth (for initial authentication)'
},
auto_complete: {
type: 'boolean',
description: 'Whether to use automatic authentication completion (default: true)'
}
},
required: ['email']
}
},
{
name: 'complete_workspace_auth',
category: 'Account Management',
description: `Complete OAuth authentication automatically by waiting for callback.
This tool waits for the user to complete OAuth authorization in their browser
and automatically captures the authorization code when the callback is received.
IMPORTANT: Only use this AFTER authenticate_workspace_account has returned an auth_url
and the user has clicked on it to start the authorization process.
The tool will wait up to 2 minutes for the authorization to complete.`,
aliases: ['wait_for_auth', 'complete_auth'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the account being authenticated'
}
},
required: ['email']
}
},
{
name: 'remove_workspace_account',
category: 'Account Management',
description: 'Remove a Google account and delete its associated authentication tokens',
aliases: ['delete_account', 'disconnect_account', 'remove_account'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Google account to remove'
}
},
required: ['email']
}
}
];
// Gmail Tools
export const gmailTools: ToolMetadata[] = [
{
name: 'manage_workspace_attachment',
category: 'Gmail/Messages',
description: `Manage attachments from Gmail messages and Calendar events using local storage.
IMPORTANT: Before any operation:
1. Verify account access with list_workspace_accounts
2. Confirm account if multiple exist
3. Validate required parameters
Operations:
- download: Download attachment to local storage
- upload: Upload new attachment
- delete: Remove attachment from storage
Storage Location:
- Files are stored in WORKSPACE_BASE_PATH/attachments
- Email attachments: .../attachments/email/
- Calendar attachments: .../attachments/calendar/
Example Flow:
1. Check account access
2. Validate message and attachment exist
3. Perform requested operation
4. Return attachment metadata`,
aliases: ['manage_attachment', 'handle_attachment', 'attachment_operation'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the workspace account'
},
action: {
type: 'string',
enum: ['download', 'upload', 'delete'],
description: 'Action to perform on the attachment'
},
source: {
type: 'string',
enum: ['email', 'calendar'],
description: 'Source type (email or calendar)'
},
messageId: {
type: 'string',
description: 'ID of the email/event containing the attachment'
},
filename: {
type: 'string',
description: 'Name of the attachment to manage'
},
content: {
type: 'string',
description: 'Base64 encoded file content (required for upload)'
}
},
required: ['email', 'action', 'source', 'messageId', 'filename']
}
},
{
name: 'search_workspace_emails',
category: 'Gmail/Messages',
description: `Search emails in a Gmail account with advanced filtering capabilities.
IMPORTANT: Before using this tool:
1. Call list_workspace_accounts to verify account access
2. If multiple accounts, confirm which account to use
3. Check required scopes include Gmail read access
Search Patterns:
1. Simple Search:
- Use individual criteria fields (from, to, subject)
- Combine multiple conditions with AND logic
- Example: from:[email protected] subject:"meeting"
2. Complex Gmail Query:
- Use content field for advanced Gmail search syntax
- Supports full Gmail search operators
- Example: "from:([email protected] OR [email protected]) subject:(meeting OR sync) -in:spam"
Common Query Patterns:
- Meeting emails: "from:(*@zoom.us OR zoom.us OR [email protected]) subject:(meeting OR sync OR invite)"
- HR/Admin: "from:(*@workday.com OR *@adp.com) subject:(time off OR PTO OR benefits)"
- Team updates: "from:(*@company.com) -from:([email protected])"
- Newsletters: "subject:(newsletter OR digest) from:(*@company.com)"
Search Tips:
- Date format: YYYY-MM-DD (e.g., "2024-02-18")
- Labels: Case-sensitive, exact match (e.g., "INBOX", "SENT")
- Wildcards: Use * for partial matches (e.g., "*@domain.com")
- Operators: OR, -, (), has:attachment, larger:size, newer_than:date
- Default maxResults: 10 (increase for broader searches)`,
aliases: ['search_emails', 'find_emails', 'query_emails'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Gmail account'
},
search: {
type: 'object',
description: 'Search criteria for filtering emails',
properties: {
from: {
oneOf: [
{ type: 'string' },
{ type: 'array', items: { type: 'string' } }
],
description: 'Search by sender email address(es)'
},
to: {
oneOf: [
{ type: 'string' },
{ type: 'array', items: { type: 'string' } }
],
description: 'Search by recipient email address(es)'
},
subject: {
type: 'string',
description: 'Search in email subject lines'
},
content: {
type: 'string',
description: 'Complex Gmail search query with full operator support (e.g., "from:(alice OR bob) subject:(meeting OR sync)")'
},
after: {
type: 'string',
description: 'Search emails after this date in YYYY-MM-DD format (e.g., "2024-01-01")'
},
before: {
type: 'string',
description: 'Search emails before this date in YYYY-MM-DD format (e.g., "2024-12-31")'
},
hasAttachment: {
type: 'boolean',
description: 'Filter emails with attachments'
},
labels: {
type: 'array',
items: { type: 'string' },
description: 'Include emails with these labels (e.g., INBOX, SENT, IMPORTANT)'
},
excludeLabels: {
type: 'array',
items: { type: 'string' },
description: 'Exclude emails with these labels'
},
includeSpam: {
type: 'boolean',
description: 'Include emails from spam/trash folders'
},
isUnread: {
type: 'boolean',
description: 'Filter by read/unread status'
}
}
},
maxResults: {
type: 'number',
description: 'Maximum number of emails to return (default: 10)'
}
},
required: ['email']
}
},
{
name: 'send_workspace_email',
category: 'Gmail/Messages',
description: `Send an email from a Gmail account.
IMPORTANT: Before sending:
0. Suggest writing a draft first, then send the draft.
1. Verify account access with list_workspace_accounts
2. Confirm sending account if multiple exist
3. Validate all recipient addresses
4. Check content for completeness
Common Patterns:
- Gather all required info before sending
- Confirm critical details with user
- Handle errors gracefully
Example Flow:
1. User requests to send email
2. Check account access
3. Collect recipient, subject, body
4. Validate all fields
5. Send and confirm success`,
aliases: ['send_email', 'send_mail', 'create_email'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address to send from'
},
to: {
type: 'array',
items: { type: 'string' },
description: 'List of recipient email addresses'
},
subject: {
type: 'string',
description: 'Email subject'
},
body: {
type: 'string',
description: 'Email body content'
},
cc: {
type: 'array',
items: { type: 'string' },
description: 'List of CC recipient email addresses'
},
bcc: {
type: 'array',
items: { type: 'string' },
description: 'List of BCC recipient email addresses'
}
},
required: ['email', 'to', 'subject', 'body']
}
},
{
name: 'get_workspace_gmail_settings',
category: 'Gmail/Settings',
description: `Get Gmail settings and profile information for a workspace account.
IMPORTANT: Always verify account access first with list_workspace_accounts.
Common Uses:
- Check account configuration
- Verify email settings
- Access profile information
Response includes:
- Language settings
- Signature settings
- Vacation responder status
- Filters and forwarding
- Other account preferences`,
aliases: ['get_gmail_settings', 'gmail_settings', 'get_mail_settings'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Gmail account'
}
},
required: ['email']
}
},
{
name: 'manage_workspace_draft',
category: 'Gmail/Drafts',
description: `Manage Gmail drafts with CRUD operations and sending.
IMPORTANT: Before any operation:
1. Verify account access with list_workspace_accounts
2. Confirm account if multiple exist
3. Validate required data for operation
Operations:
- create: Create a new draft
- read: Get a specific draft or list all drafts
- update: Modify an existing draft
- delete: Remove a draft
- send: Send an existing draft
Features:
- New email drafts
- Reply drafts with threading
- Draft modifications
- Draft sending
Example Flow:
1. Check account access
2. Perform desired operation
3. Confirm success`,
aliases: ['manage_draft', 'draft_operation', 'handle_draft'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Gmail account'
},
action: {
type: 'string',
enum: ['create', 'read', 'update', 'delete', 'send'],
description: 'Operation to perform'
},
draftId: {
type: 'string',
description: 'Draft ID (required for read/update/delete/send)'
},
data: {
type: 'object',
properties: {
to: {
type: 'array',
items: { type: 'string' },
description: 'List of recipient email addresses'
},
subject: {
type: 'string',
description: 'Email subject'
},
body: {
type: 'string',
description: 'Email body content'
},
cc: {
type: 'array',
items: { type: 'string' },
description: 'List of CC recipient email addresses'
},
bcc: {
type: 'array',
items: { type: 'string' },
description: 'List of BCC recipient email addresses'
},
replyToMessageId: {
type: 'string',
description: 'Message ID to reply to (for creating reply drafts)'
},
threadId: {
type: 'string',
description: 'Thread ID for the email (optional for replies)'
},
references: {
type: 'array',
items: { type: 'string' },
description: 'Reference message IDs for email threading'
},
inReplyTo: {
type: 'string',
description: 'Message ID being replied to (for email threading)'
}
}
}
},
required: ['email', 'action']
}
}
];
// Calendar Tools
export const calendarTools: ToolMetadata[] = [
{
name: 'list_workspace_calendar_events',
category: 'Calendar/Events',
description: `Get calendar events with optional filtering.
IMPORTANT: Before listing events:
1. Verify account access with list_workspace_accounts
2. Confirm calendar account if multiple exist
3. Check calendar access permissions
Common Usage Patterns:
- Default view: Current week's events
- Specific range: Use timeMin/timeMax
- Search: Use query for text search
Example Flows:
1. User asks "check my calendar":
- Verify account access
- Show current week by default
- Include upcoming events
2. User asks "find meetings about project":
- Check account access
- Search with relevant query
- Focus on recent/upcoming events`,
aliases: ['list_events', 'get_events', 'show_events'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the calendar owner'
},
query: {
type: 'string',
description: 'Optional text search within events'
},
maxResults: {
type: 'number',
description: 'Maximum number of events to return (default: 10)'
},
timeMin: {
type: 'string',
description: 'Start of time range to search (ISO date string)'
},
timeMax: {
type: 'string',
description: 'End of time range to search (ISO date string)'
}
},
required: ['email']
}
},
{
name: 'get_workspace_calendar_event',
category: 'Calendar/Events',
description: 'Get a single calendar event by ID',
aliases: ['get_event', 'view_event', 'show_event'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the calendar owner'
},
eventId: {
type: 'string',
description: 'Unique identifier of the event to retrieve'
}
},
required: ['email', 'eventId']
}
},
{
name: 'manage_workspace_calendar_event',
category: 'Calendar/Events',
description: `Manage calendar event responses and updates including accept/decline, propose new times, and update event times.
IMPORTANT: Before managing events:
1. Verify account access with list_workspace_accounts
2. Confirm calendar account if multiple exist
3. Verify event exists and is modifiable
Common Actions:
- Accept/Decline invitations
- Propose alternative times
- Update existing events
- Add comments to responses
Example Flow:
1. Check account access
2. Verify event exists
3. Perform desired action
4. Confirm changes applied`,
aliases: ['manage_event', 'update_event', 'respond_to_event'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the calendar owner'
},
eventId: {
type: 'string',
description: 'ID of the event to manage'
},
action: {
type: 'string',
enum: ['accept', 'decline', 'tentative', 'propose_new_time', 'update_time'],
description: 'Action to perform on the event'
},
comment: {
type: 'string',
description: 'Optional comment to include with the response'
},
newTimes: {
type: 'array',
items: {
type: 'object',
properties: {
start: {
type: 'object',
properties: {
dateTime: {
type: 'string',
description: 'Start time (ISO date string)'
},
timeZone: {
type: 'string',
description: 'Timezone for start time'
}
},
required: ['dateTime']
},
end: {
type: 'object',
properties: {
dateTime: {
type: 'string',
description: 'End time (ISO date string)'
},
timeZone: {
type: 'string',
description: 'Timezone for end time'
}
},
required: ['dateTime']
}
},
required: ['start', 'end']
},
description: 'New proposed times for the event'
}
},
required: ['email', 'eventId', 'action']
}
},
{
name: 'create_workspace_calendar_event',
category: 'Calendar/Events',
description: `Create a new calendar event.
IMPORTANT: Before creating events:
1. Verify account access with list_workspace_accounts
2. Confirm calendar account if multiple exist
3. Validate all required details
Required Formats:
- Times: ISO-8601 (e.g., "2024-02-18T15:30:00-06:00")
- Timezone: IANA identifier (e.g., "America/Chicago")
- Recurrence: RRULE format (e.g., "RRULE:FREQ=WEEKLY;COUNT=10")
Common Patterns:
1. Single Event:
- Collect title, time, attendees
- Check for conflicts
- Create and confirm
2. Recurring Event:
- Validate recurrence pattern
- Check series conflicts
- Create with RRULE
Response includes:
- Created event ID
- Scheduling conflicts
- Attendee responses`,
aliases: ['create_event', 'new_event', 'schedule_event'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the calendar owner'
},
summary: {
type: 'string',
description: 'Event title'
},
description: {
type: 'string',
description: 'Optional event description'
},
start: {
type: 'object',
properties: {
dateTime: {
type: 'string',
description: 'Event start time as ISO-8601 string (e.g., "2024-02-18T15:30:00-06:00")'
},
timeZone: {
type: 'string',
description: 'IANA timezone identifier (e.g., "America/Chicago", "Europe/London")'
}
},
required: ['dateTime']
},
end: {
type: 'object',
properties: {
dateTime: {
type: 'string',
description: 'Event end time (ISO date string)'
},
timeZone: {
type: 'string',
description: 'Timezone for end time'
}
},
required: ['dateTime']
},
attendees: {
type: 'array',
items: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Attendee email address'
}
},
required: ['email']
},
description: 'Optional list of event attendees'
},
recurrence: {
type: 'array',
items: { type: 'string' },
description: 'RRULE strings for recurring events (e.g., ["RRULE:FREQ=WEEKLY"])'
}
},
required: ['email', 'summary', 'start', 'end']
}
},
{
name: 'delete_workspace_calendar_event',
category: 'Calendar/Events',
description: `Delete a calendar event with options for recurring events.
For recurring events, you can specify a deletion scope:
- "entire_series": Removes all instances of the recurring event (default)
- "this_and_following": Removes the selected instance and all future occurrences while preserving past instances
This provides more granular control over calendar management and prevents accidental deletion of entire event series.`,
aliases: ['delete_event', 'remove_event', 'cancel_event'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the calendar owner'
},
eventId: {
type: 'string',
description: 'ID of the event to delete'
},
sendUpdates: {
type: 'string',
enum: ['all', 'externalOnly', 'none'],
description: 'Whether to send update notifications'
},
deletionScope: {
type: 'string',
enum: ['entire_series', 'this_and_following'],
description: 'For recurring events, specifies which instances to delete'
}
},
required: ['email', 'eventId']
}
}
];
// Label Management Tools
export const labelTools: ToolMetadata[] = [
{
name: 'manage_workspace_label',
category: 'Gmail/Labels',
description: `Manage Gmail labels with CRUD operations.
IMPORTANT: Before any operation:
1. Verify account access with list_workspace_accounts
2. Confirm account if multiple exist
Operations:
- create: Create a new label
- read: Get a specific label or list all labels
- update: Modify an existing label
- delete: Remove a label
Features:
- Nested labels: Use "/" (e.g., "Work/Projects")
- Custom colors: Hex codes (e.g., "#000000")
- Visibility options: Show/hide in lists
Limitations:
- Cannot create/modify system labels (INBOX, SENT, SPAM)
- Label names must be unique
Example Flow:
1. Check account access
2. Perform desired operation
3. Confirm success`,
aliases: ['manage_label', 'label_operation', 'handle_label'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Gmail account'
},
action: {
type: 'string',
enum: ['create', 'read', 'update', 'delete'],
description: 'Operation to perform'
},
labelId: {
type: 'string',
description: 'Label ID (required for read/update/delete)'
},
data: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Label name (required for create)'
},
messageListVisibility: {
type: 'string',
enum: ['show', 'hide'],
description: 'Label visibility in message list'
},
labelListVisibility: {
type: 'string',
enum: ['labelShow', 'labelHide', 'labelShowIfUnread'],
description: 'Label visibility in label list'
},
color: {
type: 'object',
properties: {
textColor: {
type: 'string',
description: 'Text color in hex format'
},
backgroundColor: {
type: 'string',
description: 'Background color in hex format'
}
}
}
}
}
},
required: ['email', 'action']
}
},
{
name: 'manage_workspace_label_assignment',
category: 'Gmail/Labels',
description: `Manage label assignments for Gmail messages.
IMPORTANT: Before assigning:
1. Verify account access with list_workspace_accounts
2. Confirm account if multiple exist
3. Verify message exists
4. Check label validity
Operations:
- add: Apply labels to a message
- remove: Remove labels from a message
Common Use Cases:
- Apply single label
- Remove single label
- Batch modify multiple labels
- Update system labels (e.g., mark as read)
Example Flow:
1. Check account access
2. Verify message and labels exist
3. Apply requested changes
4. Confirm modifications`,
aliases: ['assign_label', 'modify_message_labels', 'change_message_labels'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Gmail account'
},
action: {
type: 'string',
enum: ['add', 'remove'],
description: 'Whether to add or remove labels'
},
messageId: {
type: 'string',
description: 'ID of the message to modify'
},
labelIds: {
type: 'array',
items: { type: 'string' },
description: 'Array of label IDs to add or remove'
}
},
required: ['email', 'action', 'messageId', 'labelIds']
}
},
{
name: 'manage_workspace_label_filter',
category: 'Gmail/Labels',
description: `Manage Gmail label filters with CRUD operations.
IMPORTANT: Before any operation:
1. Verify account access with list_workspace_accounts
2. Confirm account if multiple exist
3. Verify label exists for create/update
4. Validate filter criteria
Operations:
- create: Create a new filter
- read: Get filters (all or by label)
- update: Modify existing filter
- delete: Remove filter
Filter Capabilities:
- Match sender(s) and recipient(s)
- Search subject and content
- Filter by attachments
- Size-based filtering
Actions Available:
- Apply label automatically
- Mark as important
- Mark as read
- Archive message
Criteria Format:
1. Simple filters:
- from: Array of email addresses
- to: Array of email addresses
- subject: String for exact match
- hasAttachment: Boolean
2. Complex queries:
- hasWords: Array of query strings
- doesNotHaveWords: Array of exclusion strings
Example Flow:
1. Check account access
2. Validate criteria
3. Perform operation
4. Verify result`,
aliases: ['manage_filter', 'handle_filter', 'filter_operation'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Gmail account'
},
action: {
type: 'string',
enum: ['create', 'read', 'update', 'delete'],
description: 'Operation to perform'
},
filterId: {
type: 'string',
description: 'Filter ID (required for update/delete)'
},
labelId: {
type: 'string',
description: 'Label ID (required for create/update)'
},
data: {
type: 'object',
properties: {
criteria: {
type: 'object',
properties: {
from: {
type: 'array',
items: { type: 'string' },
description: 'Match sender email addresses'
},
to: {
type: 'array',
items: { type: 'string' },
description: 'Match recipient email addresses'
},
subject: {
type: 'string',
description: 'Match text in subject'
},
hasWords: {
type: 'array',
items: { type: 'string' },
description: 'Match words in message body'
},
doesNotHaveWords: {
type: 'array',
items: { type: 'string' },
description: 'Exclude messages with these words'
},
hasAttachment: {
type: 'boolean',
description: 'Match messages with attachments'
},
size: {
type: 'object',
properties: {
operator: {
type: 'string',
enum: ['larger', 'smaller'],
description: 'Size comparison operator'
},
size: {
type: 'number',
description: 'Size in bytes'
}
}
}
}
},
actions: {
type: 'object',
properties: {
addLabel: {
type: 'boolean',
description: 'Apply the label'
},
markImportant: {
type: 'boolean',
description: 'Mark as important'
},
markRead: {
type: 'boolean',
description: 'Mark as read'
},
archive: {
type: 'boolean',
description: 'Archive the message'
}
},
required: ['addLabel']
}
}
}
},
required: ['email', 'action']
}
}
];
// Drive Tools
export const driveTools: ToolMetadata[] = [
{
name: 'list_drive_files',
category: 'Drive/Files',
description: `List files in a Google Drive account with optional filtering.
IMPORTANT: Before listing files:
1. Verify account access with list_workspace_accounts
2. Confirm account if multiple exist
3. Check Drive read permissions
Common Usage Patterns:
- List all files: No options needed
- List folder contents: Provide folderId
- Custom queries: Use query parameter
Example Flow:
1. Check account access
2. Apply any filters
3. Return file list with metadata`,
aliases: ['list_files', 'get_files', 'show_files'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Drive account'
},
options: {
type: 'object',
properties: {
folderId: {
type: 'string',
description: 'Optional folder ID to list contents of'
},
query: {
type: 'string',
description: 'Custom query string for filtering'
},
pageSize: {
type: 'number',
description: 'Maximum number of files to return'
},
orderBy: {
type: 'array',
items: { type: 'string' },
description: 'Sort order fields'
},
fields: {
type: 'array',
items: { type: 'string' },
description: 'Fields to include in response'
}
}
}
},
required: ['email']
}
},
{
name: 'search_drive_files',
category: 'Drive/Files',
description: `Search for files in Google Drive with advanced filtering.
IMPORTANT: Before searching:
1. Verify account access with list_workspace_accounts
2. Confirm account if multiple exist
3. Check Drive read permissions
Search Capabilities:
- Full text search across file content
- Filter by MIME type
- Filter by folder
- Include/exclude trashed files
Example Flow:
1. Check account access
2. Apply search criteria
3. Return matching files`,
aliases: ['search_files', 'find_files', 'query_files'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Drive account'
},
options: {
type: 'object',
properties: {
fullText: {
type: 'string',
description: 'Full text search query'
},
mimeType: {
type: 'string',
description: 'Filter by MIME type'
},
folderId: {
type: 'string',
description: 'Filter by parent folder ID'
},
trashed: {
type: 'boolean',
description: 'Include trashed files'
},
query: {
type: 'string',
description: 'Additional query string'
},
pageSize: {
type: 'number',
description: 'Maximum number of files to return'
}
}
}
},
required: ['email', 'options']
}
},
{
name: 'upload_drive_file',
category: 'Drive/Files',
description: `Upload a file to Google Drive.
IMPORTANT: Before uploading:
1. Verify account access with list_workspace_accounts
2. Confirm account if multiple exist
3. Check Drive write permissions
Features:
- Specify file name and type
- Place in specific folder
- Set file metadata
Example Flow:
1. Check account access
2. Validate file data
3. Upload and return file info`,
aliases: ['upload_file', 'create_file', 'add_file'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Drive account'
},
options: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name for the uploaded file'
},
content: {
type: 'string',
description: 'File content (string or base64)'
},
mimeType: {
type: 'string',
description: 'MIME type of the file'
},
parents: {
type: 'array',
items: { type: 'string' },
description: 'Parent folder IDs'
}
},
required: ['name', 'content']
}
},
required: ['email', 'options']
}
},
{
name: 'download_drive_file',
category: 'Drive/Files',
description: `Download a file from Google Drive.
IMPORTANT: Before downloading:
1. Verify account access with list_workspace_accounts
2. Confirm account if multiple exist
3. Check Drive read permissions
Features:
- Download any file type
- Export Google Workspace files
- Specify export format
Example Flow:
1. Check account access
2. Validate file exists
3. Download and return content`,
aliases: ['download_file', 'get_file_content', 'fetch_file'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Drive account'
},
fileId: {
type: 'string',
description: 'ID of the file to download'
},
mimeType: {
type: 'string',
description: 'Optional MIME type for export format'
}
},
required: ['email', 'fileId']
}
},
{
name: 'create_drive_folder',
category: 'Drive/Folders',
description: `Create a new folder in Google Drive.
IMPORTANT: Before creating:
1. Verify account access with list_workspace_accounts
2. Confirm account if multiple exist
3. Check Drive write permissions
Features:
- Create in root or subfolder
- Set folder metadata
Example Flow:
1. Check account access
2. Validate folder name
3. Create and return folder info`,
aliases: ['create_folder', 'new_folder', 'add_folder'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Drive account'
},
name: {
type: 'string',
description: 'Name for the new folder'
},
parentId: {
type: 'string',
description: 'Optional parent folder ID'
}
},
required: ['email', 'name']
}
},
{
name: 'update_drive_permissions',
category: 'Drive/Permissions',
description: `Update sharing permissions for a Drive file or folder.
IMPORTANT: Before updating:
1. Verify account access with list_workspace_accounts
2. Confirm account if multiple exist
3. Check Drive sharing permissions
Permission Types:
- User: Share with specific email
- Group: Share with Google Group
- Domain: Share with entire domain
- Anyone: Public sharing
Roles:
- owner: Full ownership rights
- organizer: Organizational rights
- fileOrganizer: File organization rights
- writer: Edit access
- commenter: Comment access
- reader: View access
Example Flow:
1. Check account access
2. Validate permission details
3. Update and return result`,
aliases: ['share_file', 'update_sharing', 'modify_permissions'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Drive account'
},
options: {
type: 'object',
properties: {
fileId: {
type: 'string',
description: 'ID of file/folder to update'
},
role: {
type: 'string',
enum: ['owner', 'organizer', 'fileOrganizer', 'writer', 'commenter', 'reader'],
description: 'Permission role to grant'
},
type: {
type: 'string',
enum: ['user', 'group', 'domain', 'anyone'],
description: 'Type of permission'
},
emailAddress: {
type: 'string',
description: 'Email address for user/group sharing'
},
domain: {
type: 'string',
description: 'Domain for domain sharing'
},
allowFileDiscovery: {
type: 'boolean',
description: 'Allow file discovery for anyone sharing'
}
},
required: ['fileId', 'role', 'type']
}
},
required: ['email', 'options']
}
},
{
name: 'delete_drive_file',
category: 'Drive/Files',
description: `Delete a file or folder from Google Drive.
IMPORTANT: Before deleting:
1. Verify account access with list_workspace_accounts
2. Confirm account if multiple exist
3. Check Drive write permissions
4. Confirm deletion is intended
Example Flow:
1. Check account access
2. Validate file exists
3. Delete and confirm`,
aliases: ['delete_file', 'remove_file', 'trash_file'],
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the Drive account'
},
fileId: {
type: 'string',
description: 'ID of the file/folder to delete'
}
},
required: ['email', 'fileId']
}
}
];
// Define Contacts Tools
export const contactsTools: ToolMetadata[] = [
{
name: "get_workspace_contacts",
category: "Contacts",
description: `Retrieve contacts from a Google account.
IMPORTANT: Before using this tool:
1. Verify account access with list_workspace_accounts
2. Confirm account if multiple exist
3. Check required scopes include Contacts read access
Parameters:
- email: The Google account email to access contacts from
- personFields: Required fields to include in the response (e.g. "names,emailAddresses,phoneNumbers")
- pageSize: Optional maximum number of contacts to return
- pageToken: Optional token for pagination (to get the next page)
Example Usage:
1. Call list_workspace_accounts to check for valid accounts
2. Call get_workspace_contacts with required parameters
3. Process results and use pagination for large contact lists
Common personFields Values:
- Basic info: "names,emailAddresses,phoneNumbers"
- Extended: "names,emailAddresses,phoneNumbers,addresses,organizations"
- All data: "names,emailAddresses,phoneNumbers,addresses,organizations,biographies,birthdays,photos"`,
aliases: ["get_contacts", "list_contacts", "fetch_contacts"],
inputSchema: {
type: "object",
properties: {
email: {
type: "string",
description: "Email address of the Google account"
},
personFields: {
type: "string",
description: 'Comma-separated fields to include in the response (e.g. "names,emailAddresses,phoneNumbers")'
},
pageSize: {
type: "number",
description: "Maximum number of contacts to return (default: 100)"
},
pageToken: {
type: "string",
description: "Page token from a previous response (for pagination)"
}
},
required: ["email", "personFields"]
}
}
];
// Export all tools combined
export const allTools: ToolMetadata[] = [
...accountTools,
...gmailTools,
...calendarTools,
...labelTools,
...driveTools,
...contactsTools
];
```