#
tokens: 49369/50000 96/127 files (page 1/3)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 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

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Build output
build/
dist/
*.tsbuildinfo

# Environment
.env
.env.*

# Credentials
config/gauth.json
config/accounts.json
config/credentials/
*.token.json

# IDE
.idea/
.vscode/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Logs
logs/
*.log

# Test coverage
coverage/
commit-message.txt

```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
# Version control
.git
.gitignore
.github

# Dependencies
node_modules
npm-debug.log*
.npm
.npmrc

# Development files
.vscode
.idea
*.log
logs
coverage
.nyc_output
*.tsbuildinfo

# Documentation and markdown
*.md
!README.md
docs
cline_docs
ARCHITECTURE.md
CODE_OF_CONDUCT.md
CONTRIBUTING.md
LICENSE
SECURITY.md
TODO.md

# Test files
__tests__
__fixtures__
__helpers__
__mocks__
*.test.*
*.spec.*
jest.config.*
jest.setup.*
test
tests

# Configuration and build files
.eslintrc*
.prettierrc*
.editorconfig
*.config.js
*.config.cjs
*.config.mjs
config/*
!config/*.example.json

# Source maps
*.map

# Build artifacts
dist
build
coverage
.nyc_output

# Environment files
.env*
.dockerenv

# Docker
.docker
docker-compose*
Dockerfile*
.dockerignore

# Temporary files
*.swp
*.swo
.DS_Store
Thumbs.db

```

--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------

```json
{
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "env": {
    "node": true,
    "es6": true
  },
  "parserOptions": {
    "ecmaVersion": 2020,
    "sourceType": "module"
  },
  "rules": {
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/no-explicit-any": "off",
    "@typescript-eslint/no-unused-vars": ["warn", { 
      "argsIgnorePattern": "^_|^method$|^config$|^email$|^extra$",
      "varsIgnorePattern": "^_"
    }],
    "@typescript-eslint/no-var-requires": "off"
  },
  "overrides": [
    {
      "files": ["**/__tests__/**/*", "**/__mocks__/**/*", "**/__fixtures__/**/*"],
      "rules": {
        "@typescript-eslint/no-unused-vars": "off"
      }
    }
  ]
}

```

--------------------------------------------------------------------------------
/config/credentials/README.md:
--------------------------------------------------------------------------------

```markdown
# Credentials Directory

This directory is used to store OAuth token files for authenticated Google accounts.
Token files are automatically created and managed by the application.

Note: The contents of this directory should not be committed to version control.
Token files are stored with the pattern: `[sanitized-email].token.json`

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Google Workspace MCP Server

![Robot Assistant](https://raw.githubusercontent.com/aaronsb/google-workspace-mcp/main/docs/assets/robot-assistant.png)

This Model Context Protocol (MCP) server puts you in control of your Google Workspace. Once you connect your account - a simple, secure process that takes just a minute - you're ready to go. Behind the scenes, it keeps your connection safe and active, so you can focus on getting things done instead of managing logins and permissions.

Take command of your Gmail inbox in ways you never thought possible. Want that proposal from last quarter? Found in seconds. Drowning in newsletters? They'll sort themselves into folders automatically. Need to track responses to an important thread? Labels and filters do the work for you. From drafting the perfect email to managing conversations with your team, everything just clicks into place. With streamlined attachment handling, you can easily find and manage email attachments while the system takes care of all the complex metadata behind the scenes.

Your calendar becomes a trusted ally in the daily juggle. No more double-booked meetings or timezone confusion. Planning a team sync? It spots the perfect time slots. Running a recurring workshop? Set it up once, and you're done. Even when plans change, finding new times that work for everyone is quick and painless. The days of endless "when are you free?" emails are over.

Turn Google Drive from a file dump into your digital command center. Every document finds its place, every folder tells a story. Share files with exactly the right people - no more "who can edit this?" confusion. Looking for that presentation from last week's meeting? Search not just names, but what's inside your files. Whether you're organizing a small project or managing a mountain of documents, everything stays right where you need it.

## Key Features

- **Gmail Management**: Search, send, organize emails with advanced filtering and label management
- **Calendar Operations**: Create, update, and manage events with full scheduling capabilities
- **Drive Integration**: Upload, download, search, and manage files with permission controls
- **Contact Access**: Retrieve and manage your Google contacts
- **Secure Authentication**: OAuth 2.0 flow with automatic token refresh
- **Multi-Account Support**: Manage multiple Google accounts simultaneously

## Quick Start

### Prerequisites

1. **Google Cloud Project Setup**:
   - Create a project in [Google Cloud Console](https://console.cloud.google.com)
   - Enable Gmail API, Calendar API, and Drive API
   - Configure OAuth consent screen as "External"
   - Add yourself as a test user

2. **OAuth Credentials**:
   - Create OAuth 2.0 credentials
   - Choose "Web application" type
   - Set redirect URI to: `http://localhost:8080`
   - Save your Client ID and Client Secret

3. **Local Setup**:
   - Install Docker
   - Create config directory: `mkdir -p ~/.mcp/google-workspace-mcp`
       - If the directory already exists, ensure your user owns it

### Configuration

Add the server to your MCP client configuration:

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

**For Cline** (`~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`):
```json
{
  "mcpServers": {
    "google-workspace-mcp": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "-p", "8080:8080",
        "-v", "~/.mcp/google-workspace-mcp:/app/config",
        "-v", "~/Documents/workspace-mcp-files:/app/workspace",
        "-e", "GOOGLE_CLIENT_ID",
        "-e", "GOOGLE_CLIENT_SECRET",
        "-e", "LOG_MODE=strict",
        "ghcr.io/aaronsb/google-workspace-mcp:latest"
      ],
      "env": {
        "GOOGLE_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
        "GOOGLE_CLIENT_SECRET": "your-client-secret"
      }
    }
  }
}
```

**Key Configuration Notes**:
- Port mapping `-p 8080:8080` is required for OAuth callback handling
- Replace placeholder credentials with your actual Google Cloud OAuth credentials
- The `LOG_MODE=strict` setting is recommended but not required

Logging modes:
- normal (default): Uses appropriate console methods for each log level
- strict: Routes all non-JSON-RPC messages to stderr

### Authentication

1. Restart your MCP client after configuration
2. Ask your AI assistant to "add my Google account"
3. Follow the OAuth flow:
   - Click the provided authorization URL
   - Sign in to Google and grant permissions
   - Copy the authorization code from the success page
   - Provide the code back to complete authentication

## Architecture

### OAuth Flow

The server implements a secure OAuth 2.0 flow:

1. **Callback Server**: Automatically starts on `localhost:8080` to handle OAuth redirects
2. **Authorization**: Generates Google OAuth URLs for user authentication
3. **Token Management**: Securely stores and automatically refreshes access tokens
4. **Multi-Account**: Supports multiple Google accounts with isolated token storage

### File Management

Files are organized in a structured workspace:

```
~/Documents/workspace-mcp-files/
├── [[email protected]]/
│   ├── downloads/        # Files downloaded from Drive
│   └── uploads/         # Files staged for upload
├── [[email protected]]/
│   ├── downloads/
│   └── uploads/
└── shared/
    └── temp/           # Temporary files (auto-cleanup)
```

## Available Tools

### Account Management
- `list_workspace_accounts` - List configured accounts and authentication status
- `authenticate_workspace_account` - Add and authenticate Google accounts
- `remove_workspace_account` - Remove accounts and associated tokens

### Gmail Operations
- `search_workspace_emails` - Advanced email search with filtering
- `send_workspace_email` - Send emails with attachments and formatting
- `manage_workspace_draft` - Create, update, and manage email drafts
- `manage_workspace_label` - Create and manage Gmail labels
- `manage_workspace_label_assignment` - Apply/remove labels from messages
- `manage_workspace_label_filter` - Create automated label filters
- `get_workspace_gmail_settings` - Access Gmail account settings

### Calendar Operations
- `list_workspace_calendar_events` - List and search calendar events
- `get_workspace_calendar_event` - Get detailed event information
- `create_workspace_calendar_event` - Create new events with attendees
- `manage_workspace_calendar_event` - Update events and respond to invitations
- `delete_workspace_calendar_event` - Delete calendar events

### Drive Operations
- `list_drive_files` - List files with filtering and pagination
- `search_drive_files` - Full-text search across Drive content
- `upload_drive_file` - Upload files with metadata and permissions
- `download_drive_file` - Download files with format conversion
- `delete_drive_file` - Delete files and folders
- `create_drive_folder` - Create organized folder structures
- `update_drive_permissions` - Manage file sharing and permissions

### Contacts Operations
- `get_workspace_contacts` - Retrieve contact information and details

See [API Documentation](docs/API.md) for detailed usage examples.

## Development

### Local Development

For local development and testing:

```bash
# Clone the repository
git clone https://github.com/aaronsb/google-workspace-mcp.git
cd google-workspace-mcp

# Build local Docker image
./scripts/build-local.sh

# Use local image in configuration
# Replace "ghcr.io/aaronsb/google-workspace-mcp:latest" with "google-workspace-mcp:local"
```

## Troubleshooting

### Common Issues

**Authentication Errors**:
- Verify OAuth credentials are correctly configured
- Ensure APIs (Gmail, Calendar, Drive) are enabled in Google Cloud
- Check that you're added as a test user in OAuth consent screen
- Confirm redirect URI is set to `http://localhost:8080`

**Connection Issues**:
- Verify port 8080 is available and not blocked by firewall
- Ensure Docker has permission to bind to port 8080
- Check that config directory exists and has proper permissions

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

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

**Token Issues**:
- Remove and re-authenticate accounts if tokens become invalid
- Verify API scopes are properly configured in Google Cloud
- Check token expiration and refresh logic

### Getting Help

For additional support:
- Check [Error Documentation](docs/ERRORS.md)
- Review [API Examples](docs/EXAMPLES.md)
- Submit issues on GitHub

## Security

- OAuth credentials are stored securely in MCP client configuration
- Access tokens are encrypted and stored locally
- Automatic token refresh prevents credential exposure
- Each user maintains their own Google Cloud Project
- No credentials are transmitted to external servers

## License

MIT License - See [LICENSE](LICENSE) file for details.

## Contributing

Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

```

--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------

```markdown
# Security Policy

## Supported Versions

| Version | Supported          |
| ------- | ------------------ |
| 1.x.x   | :white_check_mark: |
| < 1.0   | :x:               |

## Reporting a Vulnerability

We take the security of Google Workspace MCP seriously. If you believe you have found a security vulnerability, please follow these steps:

1. **DO NOT** open a public issue on GitHub
2. Email your findings to [email protected]
3. Include detailed information about the vulnerability:
   - Description of the issue
   - Steps to reproduce
   - Potential impact
   - Suggested fix (if any)

## What to Expect

When you report a vulnerability:

1. You'll receive acknowledgment of your report within 48 hours
2. We'll investigate and provide an initial assessment within 5 business days
3. We'll keep you informed about our progress
4. Once the issue is resolved, we'll notify you and discuss public disclosure

## Security Update Policy

1. Security patches will be released as soon as possible after a vulnerability is confirmed
2. Updates will be published through:
   - NPM package updates
   - Security advisories on GitHub
   - Release notes in our changelog

## Best Practices

When using Google Workspace MCP:

1. Always use the latest version
2. Keep your OAuth credentials secure
3. Follow our security guidelines in the documentation
4. Implement proper access controls
5. Regularly audit your token usage
6. Monitor API access logs

## Security Features

Google Workspace MCP includes several security features:

1. Secure token storage
2. OAuth 2.0 implementation
3. Rate limiting
4. Input validation
5. Secure credential handling

## Disclosure Policy

- Public disclosure will be coordinated with the reporter
- We aim to release fixes before public disclosure
- Credit will be given to security researchers who report issues (unless they prefer to remain anonymous)

## Security Contact

For security-related inquiries, contact:
- Email: [email protected]
- Subject line should start with [SECURITY]

```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
# Contributing to Google Workspace MCP

Thank you for your interest in contributing to the Google Workspace MCP project! This document provides guidelines and instructions for contributing.

## Development Setup

1. Fork and clone the repository

2. Build the development Docker image:
   ```bash
   docker build -t google-workspace-mcp:local .
   ```

3. Create a local config directory:
   ```bash
   mkdir -p ~/.mcp/google-workspace-mcp
   ```

4. Set up Google Cloud OAuth credentials:
   - Create OAuth 2.0 credentials in Google Cloud Console
   - **Important**: Choose "Web application" type (not Desktop)
   - Set redirect URI to: `http://localhost:8080`
   - Note your Client ID and Client Secret

5. Run the container with your Google API credentials:
   ```bash
   docker run -i --rm \
     -p 8080:8080 \
     -v ~/.mcp/google-workspace-mcp:/app/config \
     -e GOOGLE_CLIENT_ID=your_client_id \
     -e GOOGLE_CLIENT_SECRET=your_client_secret \
     -e LOG_MODE=strict \
     google-workspace-mcp:local
   ```

Note: For local development, you can also mount the source code directory:
```bash
docker run -i --rm \
  -p 8080:8080 \
  -v ~/.mcp/google-workspace-mcp:/app/config \
  -v $(pwd)/src:/app/src \
  -e GOOGLE_CLIENT_ID=your_client_id \
  -e GOOGLE_CLIENT_SECRET=your_client_secret \
  -e LOG_MODE=strict \
  google-workspace-mcp:local
```

**Key Development Notes**:
- Port mapping `-p 8080:8080` is required for OAuth callback handling
- OAuth credentials must be "Web application" type with `http://localhost:8080` redirect URI
- The callback server automatically starts when the OAuth client initializes

## Development Workflow

1. Create a new branch for your feature/fix:
   ```bash
   git checkout -b feature/your-feature-name
   # or
   git checkout -b fix/your-fix-name
   ```

2. Make your changes following our coding standards
3. Write/update tests as needed
4. Build and test your changes:
   ```bash
   # Build the development image
   docker build -t google-workspace-mcp:local .

   # Run tests in container
   docker run -i --rm \
     -v $(pwd):/app \
     google-workspace-mcp:local \
     npm test
   ```
5. Commit your changes using conventional commit messages:
   ```
   feat: add new feature
   fix: resolve specific issue
   docs: update documentation
   test: add/update tests
   refactor: code improvements
   ```

## Coding Standards

- Use TypeScript for all new code
- Follow existing code style and formatting
- Maintain 100% test coverage for new code
- Document all public APIs using JSDoc comments
- Use meaningful variable and function names
- Keep functions focused and modular
- Add comments for complex logic

## Testing Requirements

- Write unit tests for all new functionality
- Use Jest for testing
- Mock external dependencies
- Test both success and error cases
- Maintain existing test coverage
- Run the full test suite before submitting PR

## Pull Request Process

1. Update documentation for any new features or changes
2. Ensure all tests pass locally
3. Update CHANGELOG.md if applicable
4. Submit PR with clear description of changes
5. Address any review feedback
6. Ensure CI checks pass
7. Squash commits if requested

## Additional Resources

- [Architecture Documentation](ARCHITECTURE.md)
- [API Documentation](docs/API.md)
- [Error Handling](docs/ERRORS.md)
- [Examples](docs/EXAMPLES.md)

## Questions or Need Help?

Feel free to open an issue for:
- Bug reports
- Feature requests
- Questions about the codebase
- Suggestions for improvements

## License

By contributing, you agree that your contributions will be licensed under the MIT License.

```

--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------

```markdown
# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
  overall community

Examples of unacceptable behavior include:

* The use of sexualized language or imagery, and sexual attention or
  advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
  address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[email protected]. All complaints will be reviewed and investigated promptly
and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series of
actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression towar

```

--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------

```yaml
github: [aaronsb]

```

--------------------------------------------------------------------------------
/src/__helpers__/package.json:
--------------------------------------------------------------------------------

```json
{
  "type": "commonjs"
}

```

--------------------------------------------------------------------------------
/jest.setup.cjs:
--------------------------------------------------------------------------------

```
// Jest setup file
require('@jest/globals');

```

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

```typescript
export { GmailService } from './services/base.js';

```

--------------------------------------------------------------------------------
/.github/config.yml:
--------------------------------------------------------------------------------

```yaml
description: A Model Context Protocol (MCP) server that provides authenticated access to Google Workspace APIs, offering comprehensive Gmail and Calendar functionality
```

--------------------------------------------------------------------------------
/src/__mocks__/@modelcontextprotocol/sdk/server/stdio.js:
--------------------------------------------------------------------------------

```javascript
export class StdioServerTransport {
  constructor() {}

  async connect() {
    return Promise.resolve();
  }

  async disconnect() {
    return Promise.resolve();
  }
}

```

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

```typescript
import { DriveService } from '../../modules/drive/service.js';

// Create singleton instance
const driveService = new DriveService();

// Export singleton instance
export { driveService };

```

--------------------------------------------------------------------------------
/src/__mocks__/@modelcontextprotocol/sdk/server/index.js:
--------------------------------------------------------------------------------

```javascript
export class Server {
  constructor(config, options) {
    this.config = config;
    this.options = options;
  }

  async connect() {
    return Promise.resolve();
  }

  setRequestHandler() {
    // Mock implementation
  }
}

```

--------------------------------------------------------------------------------
/src/utils/account.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Validate email address format
 */
export function validateEmail(email: string): void {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    throw new Error(`Invalid email address: ${email}`);
  }
}

```

--------------------------------------------------------------------------------
/config/gauth.example.json:
--------------------------------------------------------------------------------

```json
{
  "client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",
  "client_secret": "YOUR_CLIENT_SECRET",
  "redirect_uri": "http://localhost:8080",
  "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth",
  "token_uri": "https://oauth2.googleapis.com/token"
}

```

--------------------------------------------------------------------------------
/src/types/modelcontextprotocol__sdk.d.ts:
--------------------------------------------------------------------------------

```typescript
declare module '@modelcontextprotocol/sdk' {
  export class McpError extends Error {
    constructor(code: ErrorCode, message: string, details?: Record<string, any>);
  }

  export enum ErrorCode {
    InternalError = 'INTERNAL_ERROR',
    InvalidRequest = 'INVALID_REQUEST',
    MethodNotFound = 'METHOD_NOT_FOUND',
    InvalidParams = 'INVALID_PARAMS'
  }
}

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import { GSuiteServer } from './tools/server.js';

// Start server with proper shutdown handling
const server = new GSuiteServer();

// Handle process signals
process.on('SIGINT', () => {
  process.exit(0);
});

process.on('SIGTERM', () => {
  process.exit(0);
});

// Start with error handling
server.run().catch(error => {
  console.error('Fatal Error:', error);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "node16",
    "moduleResolution": "node16",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "build",
    "rootDir": "src",
    "declaration": true,
    "sourceMap": true,
    "typeRoots": ["./node_modules/@types", "./src/types"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build"]
}

```

--------------------------------------------------------------------------------
/src/modules/contacts/scopes.ts:
--------------------------------------------------------------------------------

```typescript
import { scopeRegistry } from "../../modules/tools/scope-registry.js";

// Define Contacts scopes as constants
// Reference: https://developers.google.com/people/api/rest/v1/people.connections/list (and other People API docs)
export const CONTACTS_SCOPES = {
  READONLY: "https://www.googleapis.com/auth/contacts.readonly",
  // Add other scopes like write/modify later if needed
  // CONTACTS: 'https://www.googleapis.com/auth/contacts'
};

// Register the contacts scopes with the scope registry
scopeRegistry.registerScope("contacts", CONTACTS_SCOPES.READONLY);

```

--------------------------------------------------------------------------------
/src/modules/contacts/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Contacts module entry point.
 * Exports types, services, and initialization functions for the contacts module.
 */

// Export types
export * from './types.js';

// Export scopes
export { CONTACTS_SCOPES } from './scopes.js';

/**
 * Initialize the contacts module.
 * This function is called during server startup to set up any required resources.
 */
export async function initializeContactsModule(): Promise<void> {
  // Currently no async initialization needed
  // This function is a placeholder for future initialization logic
  return Promise.resolve();
}

```

--------------------------------------------------------------------------------
/src/scripts/health-check.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { GSuiteServer } from '../tools/server.js';
import logger from '../utils/logger.js';

async function checkHealth() {
  try {
    const server = new GSuiteServer();
    
    // Attempt to start server
    await server.run();
    
    // If we get here without errors, consider it healthy
    logger.info('Health check passed');
    process.exit(0);
  } catch (error) {
    logger.error('Health check failed:', error);
    process.exit(1);
  }
}

checkHealth().catch(error => {
  logger.error('Fatal error during health check:', error);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    required:
      - configDir
    properties:
      configDir:
        type: string
        description: The directory path where 'gauth.json' and 'accounts.json' are located.
  commandFunction:
    # A function that produces the CLI command to start the MCP on stdio.
    |-
    (config) => ({ command: 'docker', args: ['run', '-v', `${config.configDir}:/app/config`, 'gsuite-mcp'] })

```

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

```typescript
import { GmailService } from './services/base.js';
import { GmailError } from './types.js';
import { DraftService } from './services/draft.js';

export { GmailService, GmailError, DraftService };

let gmailService: GmailService | null = null;

export async function initializeGmailModule(): Promise<void> {
  gmailService = new GmailService();
  await gmailService.initialize();
}

export function getGmailService(): GmailService {
  if (!gmailService) {
    throw new GmailError(
      'Gmail module not initialized',
      'MODULE_NOT_INITIALIZED',
      'Call initializeGmailModule before using the Gmail service'
    );
  }
  return gmailService;
}

```

--------------------------------------------------------------------------------
/config/accounts.example.json:
--------------------------------------------------------------------------------

```json
{
  "accounts": [
    {
      "email": "[email protected]",
      "category": "personal",
      "description": "Personal Google Account",
      "auth_status": {
        "valid": true,
        "token": {
          "access_token": "YOUR_ACCESS_TOKEN",
          "refresh_token": "YOUR_REFRESH_TOKEN",
          "scope": "https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/gmail.send",
          "token_type": "Bearer",
          "expiry_date": 1737951212015,
          "last_refresh": 1737947613015
        }
      }
    },
    {
      "email": "[email protected]",
      "category": "work",
      "description": "Work Google Account"
    }
  ]
}

```

--------------------------------------------------------------------------------
/src/__mocks__/@modelcontextprotocol/sdk/types.ts:
--------------------------------------------------------------------------------

```typescript
export class McpError extends Error {
  code: string;
  details?: Record<string, any>;

  constructor(code: string, message: string, details?: Record<string, any>) {
    super(message);
    this.code = code;
    this.details = details;
    this.name = 'McpError';
  }
}

export enum ErrorCode {
  InternalError = 'INTERNAL_ERROR',
  InvalidRequest = 'INVALID_REQUEST',
  MethodNotFound = 'METHOD_NOT_FOUND',
  InvalidParams = 'INVALID_PARAMS',
  AuthenticationRequired = 'AUTHENTICATION_REQUIRED',
  AuthenticationFailed = 'AUTHENTICATION_FAILED',
  PermissionDenied = 'PERMISSION_DENIED',
  ResourceNotFound = 'RESOURCE_NOT_FOUND',
  ServiceUnavailable = 'SERVICE_UNAVAILABLE',
  ParseError = 'PARSE_ERROR'
}

```

--------------------------------------------------------------------------------
/src/__mocks__/googleapis.ts:
--------------------------------------------------------------------------------

```typescript
import { jest } from '@jest/globals';

export const google = {
  drive: jest.fn().mockReturnValue({
    files: {
      list: jest.fn(),
      create: jest.fn(),
      get: jest.fn(),
      delete: jest.fn(),
      export: jest.fn()
    },
    permissions: {
      create: jest.fn()
    }
  }),
  gmail: jest.fn().mockReturnValue({
    users: {
      messages: {
        list: jest.fn(),
        get: jest.fn(),
        send: jest.fn()
      },
      drafts: {
        create: jest.fn(),
        list: jest.fn(),
        get: jest.fn(),
        send: jest.fn()
      },
      getProfile: jest.fn(),
      settings: {
        getAutoForwarding: jest.fn(),
        getImap: jest.fn(),
        getLanguage: jest.fn(),
        getPop: jest.fn(),
        getVacation: jest.fn()
      }
    }
  })
};

```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------

```markdown
---
name: Bug Report
about: Create a report to help us improve
title: '[BUG] '
labels: bug
assignees: ''
---

## Bug Description
A clear and concise description of what the bug is.

## Steps To Reproduce
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

## Expected Behavior
A clear and concise description of what you expected to happen.

## Actual Behavior
A clear and concise description of what actually happened.

## Screenshots
If applicable, add screenshots to help explain your problem.

## Environment
- OS: [e.g. Linux, macOS, Windows]
- Node.js Version: [e.g. 18.15.0]
- NPM Version: [e.g. 9.5.0]
- Google Workspace MCP Version: [e.g. 1.0.0]

## Additional Context
Add any other context about the problem here, such as:
- Related issues
- Error messages/logs
- Configuration details

```

--------------------------------------------------------------------------------
/cline_docs/productContext.md:
--------------------------------------------------------------------------------

```markdown
# Product Context

## Purpose
This project implements a Model Context Protocol (MCP) server that provides authenticated access to Google Workspace APIs, specifically focusing on Gmail and Calendar functionality.

## Problems Solved
1. Provides a standardized interface for AI systems to interact with Google Workspace services
2. Handles complex OAuth authentication flows and token management
3. Manages multiple Google accounts securely
4. Simplifies integration with Gmail and Calendar services

## How It Works
- Implements a modular architecture focused on Gmail and Calendar functionality
- Uses OAuth 2.0 for authentication with automatic token refresh
- Provides simple verb-noun interfaces for AI agents
- Follows "simplest viable design" principle to prevent over-engineering
- Handles authentication through HTTP response codes (401/403)
- Moves OAuth mechanics into platform infrastructure

```

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

```typescript
import { driveService } from '../../services/drive/index.js';
import { DriveService } from './service.js';
import { DriveOperationResult } from './types.js';

// Export types and service
export * from './types.js';
export * from './scopes.js';
export { DriveService };

// Get singleton instance
let serviceInstance: DriveService | undefined;

export async function getDriveService(): Promise<DriveService> {
  if (!serviceInstance) {
    serviceInstance = driveService;
    await serviceInstance.ensureInitialized();
  }
  return serviceInstance;
}

// Initialize module
export async function initializeDriveModule(): Promise<void> {
  const service = await getDriveService();
  await service.ensureInitialized();
}

// Helper to handle errors consistently
export function handleDriveError(error: unknown): DriveOperationResult {
  return {
    success: false,
    error: error instanceof Error ? error.message : 'Unknown error occurred',
  };
}

```

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

```typescript
import { AccountManager } from './manager.js';
import { TokenManager } from './token.js';
import { GoogleOAuthClient } from './oauth.js';
import { Account, AccountError, TokenStatus, AccountModuleConfig } from './types.js';

// Create singleton instance
let accountManager: AccountManager | null = null;

export async function initializeAccountModule(config?: AccountModuleConfig): Promise<AccountManager> {
  if (!accountManager) {
    accountManager = new AccountManager(config);
    await accountManager.initialize();
  }
  return accountManager;
}

export function getAccountManager(): AccountManager {
  if (!accountManager) {
    throw new AccountError(
      'Account module not initialized',
      'MODULE_NOT_INITIALIZED',
      'Call initializeAccountModule before using the account manager'
    );
  }
  return accountManager;
}

export {
  AccountManager,
  TokenManager,
  GoogleOAuthClient,
  Account,
  AccountError,
  TokenStatus,
  AccountModuleConfig
};

```

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

```typescript
import { drive_v3 } from 'googleapis';

export type DriveFile = drive_v3.Schema$File;
export type DriveFileList = drive_v3.Schema$FileList;
export type DrivePermission = drive_v3.Schema$Permission;

export interface FileUploadOptions {
  name: string;
  mimeType?: string;
  parents?: string[];
  content: string | Buffer;
}

export interface FileDownloadOptions {
  fileId: string;
  mimeType?: string;
}

export interface FileListOptions {
  folderId?: string;
  query?: string;
  pageSize?: number;
  orderBy?: string[];
  fields?: string[];
}

export interface FileSearchOptions extends FileListOptions {
  fullText?: string;
  mimeType?: string;
  trashed?: boolean;
}

export interface PermissionOptions {
  fileId: string;
  role: 'owner' | 'organizer' | 'fileOrganizer' | 'writer' | 'commenter' | 'reader';
  type: 'user' | 'group' | 'domain' | 'anyone';
  emailAddress?: string;
  domain?: string;
  allowFileDiscovery?: boolean;
}

export interface DriveOperationResult {
  success: boolean;
  data?: any;
  error?: string;
  mimeType?: string;
  filePath?: string;
}

```

--------------------------------------------------------------------------------
/src/__mocks__/logger.ts:
--------------------------------------------------------------------------------

```typescript
// Mock logger that respects LOG_MODE setting
const LOG_MODE = (process.env.LOG_MODE || 'normal') as 'normal' | 'strict';

const isJsonRpc = (msg: any): boolean => {
  if (typeof msg !== 'string') return false;
  return msg.startsWith('{"jsonrpc":') || msg.startsWith('{"id":');
};

export default {
  error: (...args: any[]) => process.stderr.write(args.join(' ') + '\n'),
  warn: (...args: any[]) => {
    if (LOG_MODE === 'strict' || isJsonRpc(args[0])) {
      process.stderr.write(args.join(' ') + '\n');
    } else {
      process.stdout.write('[WARN] ' + args.join(' ') + '\n');
    }
  },
  info: (...args: any[]) => {
    if (LOG_MODE === 'strict' || isJsonRpc(args[0])) {
      process.stderr.write(args.join(' ') + '\n');
    } else {
      process.stdout.write('[INFO] ' + args.join(' ') + '\n');
    }
  },
  debug: (...args: any[]) => {
    if (!process.env.DEBUG) return;
    if (LOG_MODE === 'strict' || isJsonRpc(args[0])) {
      process.stderr.write(args.join(' ') + '\n');
    } else {
      process.stdout.write('[DEBUG] ' + args.join(' ') + '\n');
    }
  }
};

```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------

```markdown
---
name: Feature Request
about: Suggest an idea for this project
title: '[FEATURE] '
labels: enhancement
assignees: ''
---

## Problem Statement
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

## Proposed Solution
A clear and concise description of what you want to happen.

## Alternative Solutions
A clear and concise description of any alternative solutions or features you've considered.

## Implementation Details
If you have specific ideas about how to implement this feature, please share them here.
Consider:
- API changes needed
- New configuration options
- Impact on existing functionality
- Security considerations

## Benefits
Describe the benefits this feature would bring to:
- Users of the library
- Developers maintaining the code
- The project's overall goals

## Additional Context
Add any other context, screenshots, or examples about the feature request here.

## Checklist
- [ ] I have searched for similar feature requests
- [ ] This feature aligns with the project's scope
- [ ] I'm willing to help implement this feature

```

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

```typescript
import { AttachmentIndexService } from './index-service.js';

/**
 * Simplified attachment information visible to AI
 */
export interface AttachmentInfo {
  name: string;
}

/**
 * Transform attachment data for AI consumption while storing full metadata
 */
export class AttachmentTransformer {
  constructor(private indexService: AttachmentIndexService) {}

  /**
   * Transform attachments for a message, storing metadata and returning simplified format
   */
  transformAttachments(messageId: string, attachments: Array<{
    id: string;
    name: string;
    mimeType: string;
    size: number;
  }>): AttachmentInfo[] {
    // Store full metadata for each attachment
    attachments.forEach(attachment => {
      this.indexService.addAttachment(messageId, attachment);
    });

    // Return simplified format for AI
    return attachments.map(attachment => ({
      name: attachment.name
    }));
  }

  /**
   * Create a refresh placeholder when attachments have expired
   */
  static createRefreshPlaceholder(): AttachmentInfo[] {
    return [{
      name: "Attachments expired - Request message again to view"
    }];
  }
}

```

--------------------------------------------------------------------------------
/jest.config.cjs:
--------------------------------------------------------------------------------

```
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
  preset: 'ts-jest/presets/default-esm',
  testEnvironment: 'node',
  extensionsToTreatAsEsm: ['.ts'],
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
    '^@modelcontextprotocol/sdk/(.*)$': '<rootDir>/src/__mocks__/@modelcontextprotocol/sdk.ts',
    '^@modelcontextprotocol/sdk$': '<rootDir>/src/__mocks__/@modelcontextprotocol/sdk.ts',
    '^src/utils/logger.js$': '<rootDir>/src/__mocks__/logger.ts',
    '^../utils/logger.js$': '<rootDir>/src/__mocks__/logger.ts',
    '^../../utils/logger.js$': '<rootDir>/src/__mocks__/logger.ts'
  },
  transform: {
    '^.+\\.tsx?$': [
      'ts-jest',
      {
        useESM: true,
        tsconfig: 'tsconfig.json'
      },
    ],
  },
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  testMatch: ['**/__tests__/**/*.test.ts'],
  transformIgnorePatterns: [
    'node_modules/(?!(googleapis|google-auth-library|@modelcontextprotocol/sdk|zod)/)'
  ],
  setupFilesAfterEnv: ['<rootDir>/src/__helpers__/testSetup.ts'],
  fakeTimers: {
    enableGlobally: true,
    now: new Date('2025-02-21T20:30:00Z').getTime()
  }
};

```

--------------------------------------------------------------------------------
/src/__mocks__/@modelcontextprotocol/sdk.ts:
--------------------------------------------------------------------------------

```typescript
export class McpError extends Error {
  code: string;
  details?: Record<string, any>;

  constructor(code: string, message: string, details?: Record<string, any>) {
    super(message);
    this.code = code;
    this.details = details;
    this.name = 'McpError';
  }
}

export enum ErrorCode {
  InternalError = 'INTERNAL_ERROR',
  InvalidRequest = 'INVALID_REQUEST',
  MethodNotFound = 'METHOD_NOT_FOUND',
  InvalidParams = 'INVALID_PARAMS',
  AuthenticationRequired = 'AUTHENTICATION_REQUIRED',
  AuthenticationFailed = 'AUTHENTICATION_FAILED',
  PermissionDenied = 'PERMISSION_DENIED',
  ResourceNotFound = 'RESOURCE_NOT_FOUND',
  ServiceUnavailable = 'SERVICE_UNAVAILABLE',
  ParseError = 'PARSE_ERROR'
}

export class Server {
  private config: any;
  public onerror: ((error: Error) => void) | undefined;

  constructor(config: any, options?: any) {
    this.config = config;
  }

  async connect(transport: any): Promise<void> {
    // Mock implementation
    return Promise.resolve();
  }

  async close(): Promise<void> {
    // Mock implementation
    return Promise.resolve();
  }

  setRequestHandler(schema: any, handler: any): void {
    // Mock implementation
  }
}

```

--------------------------------------------------------------------------------
/src/__fixtures__/accounts.ts:
--------------------------------------------------------------------------------

```typescript
export const mockAccounts = {
  accounts: [
    {
      email: '[email protected]',
      category: 'work',
      description: 'Test Work Account'
    },
    {
      email: '[email protected]',
      category: 'personal',
      description: 'Test Personal Account'
    }
  ]
};

export const mockTokens = {
  valid: {
    access_token: 'valid-token',
    refresh_token: 'refresh-token',
    expiry_date: Date.now() + 3600000
  },
  expired: {
    access_token: 'expired-token',
    refresh_token: 'refresh-token',
    expiry_date: Date.now() - 3600000
  }
};

export const mockGmailResponses = {
  messageList: {
    messages: [
      { id: 'msg1', threadId: 'thread1' },
      { id: 'msg2', threadId: 'thread2' }
    ],
    resultSizeEstimate: 2
  },
  message: {
    id: 'msg1',
    threadId: 'thread1',
    labelIds: ['INBOX'],
    snippet: 'Email snippet',
    payload: {
      headers: [
        { name: 'From', value: '[email protected]' },
        { name: 'Subject', value: 'Test Subject' },
        { name: 'To', value: '[email protected]' },
        { name: 'Date', value: '2024-01-01T00:00:00Z' }
      ],
      parts: [
        {
          mimeType: 'text/plain',
          body: { data: Buffer.from('Test content').toString('base64') }
        }
      ]
    }
  }
};

```

--------------------------------------------------------------------------------
/cline_docs/techContext.md:
--------------------------------------------------------------------------------

```markdown
# Technical Context

## Technologies Used
- TypeScript/Node.js for server implementation
- Google Workspace APIs (Gmail, Calendar)
- OAuth 2.0 for authentication
- Model Context Protocol (MCP) for AI integration

## Development Setup
1. **Required Configuration Files**
   - `config/gauth.json`: OAuth credentials
   - `config/accounts.json`: Account configurations
   - `config/credentials/`: Token storage

2. **Environment Variables**
   - AUTH_CONFIG_FILE: OAuth credentials path
   - ACCOUNTS_FILE: Account config path
   - CREDENTIALS_DIR: Token storage path

## Technical Constraints
1. **OAuth & Authentication**
   - Must handle token refresh flows
   - Requires proper scope management
   - Needs secure token storage

2. **API Limitations**
   - Gmail API rate limits
   - Calendar API quotas
   - OAuth token expiration

3. **Tool Registration**
   - Tools must be registered in both ListToolsRequestSchema and CallToolRequestSchema
   - Must follow verb-noun naming convention

4. **Error Handling**
   - Must handle auth errors (401/403)
   - Must implement automatic token refresh
   - Must provide clear error messages

5. **Security Requirements**
   - Secure credential storage
   - Token encryption
   - Environment-based configuration
   - No sensitive data in version control

```

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

```typescript
import { OAuth2Client } from 'google-auth-library';

export interface Account {
  email: string;
  category: string;
  description: string;
  auth_status?: {
    valid: boolean;
    status?: TokenStatusType;
    token?: any;  // Internal use only - not exposed to AI
    reason?: string;
    authUrl?: string;
    requiredScopes?: string[];
  };
}

export interface AccountsConfig {
  accounts: Account[];
}

export type TokenStatusType = 
  | 'NO_TOKEN'
  | 'VALID'
  | 'INVALID'
  | 'REFRESHED'
  | 'REFRESH_FAILED'
  | 'EXPIRED'
  | 'ERROR';

export interface TokenRenewalResult {
  success: boolean;
  status: TokenStatusType;
  reason?: string;
  token?: any;
  canRetry?: boolean;  // Indicates if a failed refresh can be retried later
}

export interface TokenStatus {
  valid: boolean;
  status: TokenStatusType;
  token?: any;
  reason?: string;
  authUrl?: string;
  requiredScopes?: string[];
}

export interface AuthenticationError extends AccountError {
  authUrl: string;
  requiredScopes: string[];
}

export interface AccountModuleConfig {
  accountsPath?: string;
  oauth2Client?: OAuth2Client;
}

export class AccountError extends Error {
  constructor(
    message: string,
    public code: string,
    public resolution: string
  ) {
    super(message);
    this.name = 'AccountError';
  }
}

```

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

```typescript
export interface AttachmentMetadata {
  id: string;           // Unique identifier
  name: string;         // Original filename
  mimeType: string;     // MIME type
  size: number;         // File size in bytes
  path: string;         // Local filesystem path
}

export interface AttachmentSource {
  content: string;      // Base64 content
  metadata: {
    name: string;
    mimeType: string;
    size?: number;
  };
}

export interface AttachmentResult {
  success: boolean;
  attachment?: AttachmentMetadata;
  error?: string;
}

export interface AttachmentServiceConfig {
  maxSizeBytes?: number;                    // Maximum file size (default: 25MB)
  allowedMimeTypes?: string[];             // Allowed MIME types (default: all)
  basePath?: string;                       // Base path for attachments (default: WORKSPACE_BASE_PATH/attachments)
  quotaLimitBytes?: number;                // Storage quota limit
}

export interface AttachmentValidationResult {
  valid: boolean;
  error?: string;
}

// Folder structure constants
export type AttachmentFolderType = 'attachments' | 'email' | 'calendar' | 'incoming' | 'outgoing' | 'event-files';

export const ATTACHMENT_FOLDERS: Record<string, AttachmentFolderType> = {
  ROOT: 'attachments',
  EMAIL: 'email',
  CALENDAR: 'calendar',
  INCOMING: 'incoming',
  OUTGOING: 'outgoing',
  EVENT_FILES: 'event-files'
};

```

--------------------------------------------------------------------------------
/scripts/local-entrypoint.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash

# Load environment variables from .env file
if [ -f .env ]; then
    export $(cat .env | grep -v '^#' | xargs)
fi

# Function to log error messages
log_error() {
    echo "[ERROR] $1" >&2
}

# Function to log info messages
log_info() {
    echo "[INFO] $1" >&2
}

# Validate required environment variables
if [ -z "$GOOGLE_CLIENT_ID" ]; then
    log_error "GOOGLE_CLIENT_ID environment variable is required"
    exit 1
fi

if [ -z "$GOOGLE_CLIENT_SECRET" ]; then
    log_error "GOOGLE_CLIENT_SECRET environment variable is required"
    exit 1
fi

# Set default workspace path if not provided
if [ -z "$WORKSPACE_BASE_PATH" ]; then
    export WORKSPACE_BASE_PATH="$HOME/Documents/workspace-mcp-files"
fi

# Create necessary directories
mkdir -p "$HOME/.mcp/google-workspace-mcp"
mkdir -p "$WORKSPACE_BASE_PATH"

# Link config to expected location (symlink approach)
mkdir -p /tmp/app/config
ln -sf "$HOME/.mcp/google-workspace-mcp/accounts.json" /tmp/app/config/accounts.json 2>/dev/null || true

# If accounts.json doesn't exist, create it
if [ ! -f "$HOME/.mcp/google-workspace-mcp/accounts.json" ]; then
    log_info "Creating accounts.json"
    echo '{}' > "$HOME/.mcp/google-workspace-mcp/accounts.json"
fi

# Trap signals for clean shutdown
trap 'log_info "Shutting down..."; exit 0' SIGTERM SIGINT

# Execute the main application
exec node build/index.js "$@"
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# syntax=docker/dockerfile:1.4

# Build stage
FROM --platform=$BUILDPLATFORM node:20-slim AS builder
WORKDIR /app

# Add metadata
LABEL org.opencontainers.image.source="https://github.com/aaronsb/google-workspace-mcp"
LABEL org.opencontainers.image.description="Google Workspace MCP Server"
LABEL org.opencontainers.image.licenses="MIT"

# Install dependencies first (better layer caching)
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
    npm ci --prefer-offline --no-audit --no-fund

# Copy source and build
COPY . .
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
    npm run build

# Production stage
FROM node:20-slim AS production
WORKDIR /app

# Set docker hash as environment variable
ARG DOCKER_HASH=unknown
ENV DOCKER_HASH=$DOCKER_HASH

# Copy only necessary files from builder
COPY --from=builder /app/build ./build
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/docker-entrypoint.sh ./

# Install production dependencies and set up directories
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
    npm ci --prefer-offline --no-audit --no-fund --omit=dev && \
    npm install [email protected] && \
    chmod +x build/index.js && \
    chmod +x docker-entrypoint.sh && \
    mkdir -p /app/logs && \
    chown -R 1000:1000 /app

# Switch to non-root user
USER 1000:1000

ENTRYPOINT ["./docker-entrypoint.sh"]

```

--------------------------------------------------------------------------------
/docker-entrypoint.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash

# Function to log error messages
log_error() {
    echo "[ERROR] $1" >&2
}

# Function to log info messages
log_info() {
    echo "[INFO] $1" >&2
}

# Validate required environment variables
if [ -z "$GOOGLE_CLIENT_ID" ]; then
    log_error "GOOGLE_CLIENT_ID environment variable is required"
    exit 1
fi

if [ -z "$GOOGLE_CLIENT_SECRET" ]; then
    log_error "GOOGLE_CLIENT_SECRET environment variable is required"
    exit 1
fi

# Set default workspace path if not provided
if [ -z "$WORKSPACE_BASE_PATH" ]; then
    export WORKSPACE_BASE_PATH="/app/workspace"
fi

# Trap signals for clean shutdown
trap 'log_info "Shutting down..."; exit 0' SIGTERM SIGINT

# Set environment variables
export MCP_MODE=true
export LOG_FILE="/app/logs/google-workspace-mcp.log"
export WORKSPACE_BASE_PATH="$WORKSPACE_BASE_PATH"

# Ensure /app/config/accounts.json exists, copy from example if missing, or create minimal if both missing
if [ ! -f "/app/config/accounts.json" ]; then
    if [ -f "/app/config/accounts.example.json" ]; then
        log_info "accounts.json not found, copying from accounts.example.json"
        cp /app/config/accounts.example.json /app/config/accounts.json
    else
        log_info "accounts.json and accounts.example.json not found, creating minimal accounts.json"
        echo '{ "accounts": [] }' > /app/config/accounts.json
    fi
fi

# Execute the main application
exec node build/index.js

```

--------------------------------------------------------------------------------
/cline_docs/progress.md:
--------------------------------------------------------------------------------

```markdown
# Progress Status

## What Works
- Account Manager Tests
  - loadAccounts functionality with Map structure
  - validateAccount initialization
  - AccountError types implementation
  - fs mocks for mkdir and dirname
- Calendar Service Tests
  - ISO date format handling
  - createEvent validation
  - Optional parameters testing
  - Invalid date handling
- Gmail Service Tests
  - Basic functionality verified
- Error Handling
  - OAuth client error handling
  - Debug logging for auth config
  - Error message validation

## What's Left to Build/Investigate
1. Additional Test Coverage
   - Edge cases and error scenarios
   - Token refresh flows
   - Rate limiting behavior
   - Invalid input handling
   - Concurrent operations
   - MCP server operations

2. Test Infrastructure
   - Mock implementations review
   - Test organization optimization
   - Documentation coverage
   - Error message clarity

3. Integration Testing
   - Tool registration verification
   - Request validation flows
   - Error propagation paths
   - Authentication scenarios
   - Response format validation

## Progress Status
- Account Manager Tests: ✅ Complete (14 tests)
- Calendar Service Tests: ✅ Complete (11 tests)
- Gmail Service Tests: ✅ Complete (6 tests)
- Basic Error Handling: ✅ Complete
- Edge Cases: 🔄 In Progress
- MCP Server Tests: 🔄 In Progress
- Integration Tests: 🔄 Pending
- Documentation: 🔄 Pending
- Final Review: 🔄 Pending

```

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

```typescript
import { scopeRegistry } from '../../../modules/tools/scope-registry.js';
import { DRIVE_SCOPES, registerDriveScopes, validateDriveScopes } from '../scopes.js';

describe('Drive Scopes', () => {
  beforeEach(() => {
    // Clear any existing scopes
    jest.clearAllMocks();
  });

  describe('registerDriveScopes', () => {
    it('should register all drive scopes', () => {
      registerDriveScopes();
      const registeredScopes = scopeRegistry.getAllScopes();
      
      expect(registeredScopes).toContain(DRIVE_SCOPES.FULL);
      expect(registeredScopes).toContain(DRIVE_SCOPES.READONLY);
      expect(registeredScopes).toContain(DRIVE_SCOPES.FILE);
      expect(registeredScopes).toContain(DRIVE_SCOPES.METADATA);
      expect(registeredScopes).toContain(DRIVE_SCOPES.APPDATA);
    });
  });

  describe('validateDriveScopes', () => {
    it('should return true for valid scopes', () => {
      const validScopes = [
        DRIVE_SCOPES.FULL,
        DRIVE_SCOPES.READONLY,
        DRIVE_SCOPES.FILE
      ];
      expect(validateDriveScopes(validScopes)).toBe(true);
    });

    it('should return false for invalid scopes', () => {
      const invalidScopes = [
        DRIVE_SCOPES.FULL,
        'invalid.scope',
        DRIVE_SCOPES.FILE
      ];
      expect(validateDriveScopes(invalidScopes)).toBe(false);
    });

    it('should return true for empty scope array', () => {
      expect(validateDriveScopes([])).toBe(true);
    });
  });
});

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "google-workspace-mcp",
  "version": "1.2.0",
  "description": "Google Workspace OAuth MCP Server for Google Workspace integration",
  "private": true,
  "type": "module",
  "bin": {
    "google-workspace-mcp": "./build/index.js"
  },
  "files": [
    "build"
  ],
  "scripts": {
    "build": "tsc && node --eval \"import('fs').then(fs => { ['build/index.js', 'build/scripts/setup-google-env.js', 'build/scripts/health-check.js'].forEach(f => fs.chmodSync(f, '755')); })\"",
    "type-check": "tsc --noEmit",
    "lint": "eslint \"src/**/*.ts\"",
    "watch": "tsc --watch",
    "inspector": "npx @modelcontextprotocol/inspector build/index.js",
    "start": "node build/index.js",
    "setup": "node build/scripts/setup-google-env.js",
    "test": "jest --config jest.config.cjs",
    "test:watch": "jest --config jest.config.cjs --watch"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.7.0",
    "express": "^4.18.2",
    "google-auth-library": "^9.4.1",
    "googleapis": "^129.0.0",
    "uuid": "^11.1.0"
  },
  "overrides": {
    "glob": "^11.0.1"
  },
  "devDependencies": {
    "@babel/preset-env": "^7.26.7",
    "@types/express": "^4.17.21",
    "@types/jest": "^29.5.14",
    "@types/node": "^20.11.24",
    "@types/uuid": "^10.0.0",
    "@typescript-eslint/eslint-plugin": "^6.21.0",
    "@typescript-eslint/parser": "^6.21.0",
    "babel-jest": "^29.7.0",
    "eslint": "^8.56.0",
    "jest": "^29.7.0",
    "lru-cache": "^11.0.2",
    "ts-jest": "^29.2.5",
    "uuid": "^11.1.0"
  }
}

```

--------------------------------------------------------------------------------
/cline_docs/activeContext.md:
--------------------------------------------------------------------------------

```markdown
# Active Context

## Current Task
Testing and verification of the MCP server implementation, focusing on the dev-add-tests branch.

## Recent Changes
1. Fixed Account Manager Tests:
   - Fixed loadAccounts functionality with proper Map structure handling
   - Added proper initialization in validateAccount tests
   - Added better error handling with specific AccountError types
   - Fixed fs mocks including mkdir and dirname

2. Fixed Calendar Service Tests:
   - Updated date format expectations to match ISO string format
   - Fixed createEvent response validation
   - Added comprehensive tests for optional parameters
   - Added tests for invalid date handling

3. Improved Error Handling:
   - Added better error handling in OAuth client
   - Added debug logging for auth config loading
   - Fixed error message expectations in tests

## Test Coverage Status
- Account manager: 14 tests passing
- Calendar service: 11 tests passing
- Gmail service: 6 tests passing
- Total: 31 tests passing across all suites

## Next Steps
1. Add more test cases:
   - Edge conditions and error scenarios
   - Token refresh flows
   - Rate limiting handling
   - Invalid input handling
   - Concurrent operations

2. Test MCP server operations:
   - Tool registration
   - Request validation
   - Error propagation
   - Authentication flows
   - Response formatting

3. Review and improve:
   - Error messages clarity
   - Test organization
   - Mock implementations
   - Documentation coverage

4. Final Steps:
   - Complete thorough testing
   - Review test coverage
   - Merge dev-add-tests to main

```

--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------

```markdown
## Description
Please include a summary of the changes and which issue is fixed. Include relevant motivation and context.

Fixes # (issue)

## Type of Change
Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring
- [ ] Test coverage improvement

## Testing Performed
Please describe the tests you ran to verify your changes:

1. Unit Tests:
   - [ ] Added new tests for new functionality
   - [ ] All existing tests pass
   - [ ] Test coverage maintained/improved

2. Integration Testing:
   - [ ] Tested with real Google Workspace accounts
   - [ ] Verified OAuth flow
   - [ ] Checked error handling

## Checklist
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have updated the CHANGELOG.md file
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes

## Security Considerations
- [ ] I have followed security best practices
- [ ] No sensitive information is exposed
- [ ] API keys and tokens are properly handled
- [ ] Input validation is implemented where necessary

## Additional Notes
Add any other context about the pull request here.

```

--------------------------------------------------------------------------------
/docs/automatic-oauth-flow.md:
--------------------------------------------------------------------------------

```markdown
# Automatic OAuth Flow

This document describes the automatic OAuth authentication flow implemented in PR #[number].

## Problem

Previously, users had to manually copy and paste authorization codes from the OAuth callback page back to the MCP client, which was cumbersome and error-prone.

## Solution

The OAuth callback server now automatically submits the authorization code back to itself, allowing the authentication to complete transparently.

### How It Works

1. **User initiates authentication**
   ```
   authenticate_workspace_account email="[email protected]"
   ```

2. **Server returns auth URL**
   - The callback server is already listening on localhost:8080
   - User clicks the auth URL to open Google sign-in

3. **User authorizes in browser**
   - Google redirects to `http://localhost:8080/?code=AUTH_CODE`
   - The callback page automatically POSTs the code to `/complete-auth`

4. **Automatic completion**
   - User calls `complete_workspace_auth email="[email protected]"`
   - This waits for the callback server to receive the code
   - Authentication completes automatically

### Fallback Mode

For compatibility, the manual flow still works:
- Set `auto_complete=false` in authenticate_workspace_account
- Copy the code from the success page
- Provide it with `auth_code` parameter

### Technical Details

- Added `/complete-auth` endpoint to callback server
- JavaScript in the success page automatically submits the code
- The `complete_workspace_auth` tool waits for the promise to resolve
- 2-minute timeout prevents hanging

### User Experience

The new flow shows:
1. "Authentication initiated" message
2. Success page with "Completing authentication automatically..."
3. No manual code copying required
```

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

```typescript
/**
 * Simple registry to collect OAuth scopes needed by tools.
 * Scopes are gathered at startup and used for initial auth only.
 * No validation is performed - auth issues are handled via 401 responses.
 */

export interface ToolScope {
  scope: string;
  tool: string;
}

export class ScopeRegistry {
  private static instance: ScopeRegistry;
  private scopes: Map<string, ToolScope>;
  private scopeOrder: string[]; // Maintain registration order

  private constructor() {
    this.scopes = new Map();
    this.scopeOrder = [];
  }

  static getInstance(): ScopeRegistry {
    if (!ScopeRegistry.instance) {
      ScopeRegistry.instance = new ScopeRegistry();
    }
    return ScopeRegistry.instance;
  }

  /**
   * Register a scope needed by a tool.
   * If the scope is already registered, it will not be re-registered
   * but its position in the order will be updated.
   */
  registerScope(tool: string, scope: string) {
    // Remove from order if already exists
    const existingIndex = this.scopeOrder.indexOf(scope);
    if (existingIndex !== -1) {
      this.scopeOrder.splice(existingIndex, 1);
    }

    // Add to map and order
    this.scopes.set(scope, { scope, tool });
    this.scopeOrder.push(scope);
  }

  /**
   * Get all registered scopes in their registration order.
   * This order is important for auth URL generation to ensure
   * consistent scope presentation to users.
   */
  getAllScopes(): string[] {
    // Return scopes in registration order
    return this.scopeOrder;
  }

  getToolScopes(tool: string): string[] {
    return Array.from(this.scopes.values())
      .filter(scope => scope.tool === tool)
      .map(scope => scope.scope);
  }

}

// Export a singleton instance
export const scopeRegistry = ScopeRegistry.getInstance();

```

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

```typescript
/**
 * Parameters for retrieving contacts.
 */
export interface GetContactsParams {
  email: string; // The user account email
  pageSize?: number; // Max number of contacts to return
  pageToken?: string; // Token for pagination
  // Add other parameters like sortOrder, syncToken if needed later
  personFields: string; // Required: Fields to request e.g. 'names,emailAddresses,phoneNumbers'
}

/**
 * Response structure for getting contacts.
 */
export interface GetContactsResponse {
  connections: Contact[];
  nextPageToken?: string;
  totalPeople?: number;
  totalItems?: number; // Deprecated
}

/**
 * Represents a Google Contact (Person).
 * Based on People API Person resource.
 * Reference: https://developers.google.com/people/api/rest/v1/people#Person
 */
export interface Contact {
  resourceName: string;
  etag?: string;
  names?: Name[];
  emailAddresses?: EmailAddress[];
  phoneNumbers?: PhoneNumber[];
  // Add other fields as needed (e.g. photos, addresses, organizations, etc.)
}

// --- Sub-types based on People API ---

export interface Name {
  displayName?: string;
  familyName?: string;
  givenName?: string;
  // ... other name fields
}

export interface EmailAddress {
  value?: string;
  type?: string; // e.g. 'home', 'work'
  formattedType?: string;
  // ... other email fields
}

export interface PhoneNumber {
  value?: string;
  canonicalForm?: string;
  type?: string; // e.g. 'mobile', 'home', 'work'
  formattedType?: string;
  // ... other phone fields
}

/**
 * Base error class for Contacts service.
 */
export class ContactsError extends Error {
  code: string;
  details?: string;

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

```

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

```typescript
// Account Types
export interface Account {
  email: string;
  category: string;
  description: string;
  auth_status?: {
    has_token: boolean;
    scopes?: string[];
    expires?: number;
  };
}

export interface AccountsConfig {
  accounts: Account[];
}

// Token Types
export interface TokenData {
  access_token: string;
  refresh_token: string;
  scope: string;
  token_type: string;
  expiry_date: number;
  last_refresh: number;
}

// OAuth Config Types
export interface OAuthConfig {
  client_id: string;
  client_secret: string;
  auth_uri: string;
  token_uri: string;
}

// Authentication Types
export interface GoogleAuthParams {
  email: string;
  category?: string;
  description?: string;
  required_scopes: string[];
  auth_code?: string;
}

// API Request Types
export interface GoogleApiRequestParams extends GoogleAuthParams {
  api_endpoint: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  params?: Record<string, any>;
}

// API Response Types
export type GoogleApiResponse = 
  | {
      status: 'success';
      data?: any;
      message?: string;
    }
  | {
      status: 'auth_required';
      auth_url: string;
      message?: string;
      instructions: string;
    }
  | {
      status: 'refreshing';
      message: string;
    }
  | {
      status: 'error';
      error: string;
      message?: string;
      resolution?: string;
    };

// Error Types
export class GoogleApiError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly resolution?: string
  ) {
    super(message);
    this.name = 'GoogleApiError';
  }
}

// Utility Types
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

export interface ApiRequestParams {
  endpoint: string;
  method: HttpMethod;
  params?: Record<string, any>;
  token: string;
}

```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
name: CI

on:
  push:
    branches: [ main, fix/* ]
  pull_request:
    branches: [ main ]
  workflow_dispatch:

env:
  BUILDX_NO_DEFAULT_LOAD: true

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '20.x'
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Run tests
      run: npm test
      env:
        GOOGLE_CLIENT_ID: test-id
        GOOGLE_CLIENT_SECRET: test-secret
        CONFIG_DIR: ./test-config
        WORKSPACE_BASE_PATH: ./test-workspace

  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '20.x'
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Build
      run: npm run build

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
      with:
        platforms: linux/amd64,linux/arm64

    - name: Test Docker build
      uses: docker/build-push-action@v5
      with:
        context: .
        push: false
        load: false
        tags: google-workspace-mcp:test
        platforms: linux/amd64,linux/arm64
        cache-from: |
          type=gha,scope=${{ github.ref_name }}-amd64
          type=gha,scope=${{ github.ref_name }}-arm64
        cache-to: |
          type=gha,mode=min,scope=${{ github.ref_name }}-amd64
          type=gha,mode=min,scope=${{ github.ref_name }}-arm64
        outputs: type=image,name=google-workspace-mcp:test

  lint:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '20.x'
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Run ESLint
      run: npm run lint

```

--------------------------------------------------------------------------------
/src/utils/service-initializer.ts:
--------------------------------------------------------------------------------

```typescript
import logger from './logger.js';
import { initializeAccountModule } from '../modules/accounts/index.js';
import { initializeGmailModule } from '../modules/gmail/index.js';
import { initializeCalendarModule } from '../modules/calendar/index.js';
import { initializeDriveModule } from '../modules/drive/index.js';
import { initializeContactsModule } from '../modules/contacts/index.js';
import { registerGmailScopes } from '../modules/gmail/scopes.js';
import { registerCalendarScopes } from '../modules/calendar/scopes.js';
import { registerDriveScopes } from '../modules/drive/scopes.js';
import { CONTACTS_SCOPES } from '../modules/contacts/scopes.js';
import { scopeRegistry } from '../modules/tools/scope-registry.js';

// Function to register contacts scopes
function registerContactsScopes(): void {
  scopeRegistry.registerScope("contacts", CONTACTS_SCOPES.READONLY);
  logger.info('Contacts scopes registered');
}

export async function initializeAllServices(): Promise<void> {
  try {
    // Register all scopes first
    logger.info('Registering API scopes...');
    registerGmailScopes();
    registerCalendarScopes();
    registerDriveScopes();
    registerContactsScopes();

    // Initialize account module first as other services depend on it
    logger.info('Initializing account module...');
    await initializeAccountModule();

    // Initialize remaining services in parallel
    logger.info('Initializing service modules in parallel...');
    await Promise.all([
      initializeDriveModule().then(() => logger.info('Drive module initialized')),
      initializeGmailModule().then(() => logger.info('Gmail module initialized')),
      initializeCalendarModule().then(() => logger.info('Calendar module initialized')),
      initializeContactsModule().then(() => logger.info('Contacts module initialized'))
    ]);

    logger.info('All services initialized successfully');
  } catch (error) {
    logger.error('Failed to initialize services:', error);
    throw error;
  }
}

```

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

```typescript
import { scopeRegistry } from '../tools/scope-registry.js';

// Define Gmail scopes as constants for reuse and testing
// Reference: https://developers.google.com/gmail/api/auth/scopes
export const GMAIL_SCOPES = {
  // Core functionality scopes (read, write, modify permissions)
  READONLY: 'https://www.googleapis.com/auth/gmail.readonly',
  SEND: 'https://www.googleapis.com/auth/gmail.send',
  MODIFY: 'https://www.googleapis.com/auth/gmail.modify',
  
  // Label management scope
  LABELS: 'https://www.googleapis.com/auth/gmail.labels',
  
  // Settings management scopes
  SETTINGS_BASIC: 'https://www.googleapis.com/auth/gmail.settings.basic',
  SETTINGS_SHARING: 'https://www.googleapis.com/auth/gmail.settings.sharing'
};

/**
 * Register Gmail OAuth scopes at startup.
 * Auth issues will be handled via 401 responses rather than pre-validation.
 * 
 * IMPORTANT: The order of scope registration matters for auth URL generation.
 * Core functionality scopes (readonly, send, modify) should be registered first,
 * followed by feature-specific scopes (labels, settings).
 */
export function registerGmailScopes() {
  // Register core functionality scopes first
  scopeRegistry.registerScope('gmail', GMAIL_SCOPES.READONLY);
  scopeRegistry.registerScope('gmail', GMAIL_SCOPES.SEND);
  scopeRegistry.registerScope('gmail', GMAIL_SCOPES.MODIFY);
  
  // Register feature-specific scopes
  scopeRegistry.registerScope('gmail', GMAIL_SCOPES.LABELS);
  
  // Register settings scopes last (order matters for auth URL generation)
  scopeRegistry.registerScope('gmail', GMAIL_SCOPES.SETTINGS_BASIC);
  scopeRegistry.registerScope('gmail', GMAIL_SCOPES.SETTINGS_SHARING);
  
  // Verify all scopes are registered
  const registeredScopes = scopeRegistry.getAllScopes();
  const requiredScopes = Object.values(GMAIL_SCOPES);
  
  const missingScopes = requiredScopes.filter(scope => !registeredScopes.includes(scope));
  if (missingScopes.length > 0) {
    throw new Error(`Failed to register Gmail scopes: ${missingScopes.join(', ')}`);
  }
}

```

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

```typescript
import { scopeRegistry } from '../tools/scope-registry.js';

// Define Drive scopes as constants for reuse and testing
// Reference: https://developers.google.com/drive/api/auth/scopes
export const DRIVE_SCOPES = {
  // Full access to files and folders (create, read, update, delete)
  FULL: 'https://www.googleapis.com/auth/drive',
  
  // Read-only access to files
  READONLY: 'https://www.googleapis.com/auth/drive.readonly',
  
  // Access to files created by the app
  FILE: 'https://www.googleapis.com/auth/drive.file',
  
  // Access to metadata only
  METADATA: 'https://www.googleapis.com/auth/drive.metadata',
  
  // Access to app data folder
  APPDATA: 'https://www.googleapis.com/auth/drive.appdata',
} as const;

export type DriveScope = typeof DRIVE_SCOPES[keyof typeof DRIVE_SCOPES];

/**
 * Register Drive OAuth scopes at startup.
 * Auth issues will be handled via 401 responses rather than pre-validation.
 * 
 * IMPORTANT: The order of scope registration matters for auth URL generation.
 * Core functionality scopes should be registered first,
 * followed by feature-specific scopes.
 */
export function registerDriveScopes(): void {
  // Register core functionality scopes first
  scopeRegistry.registerScope('drive', DRIVE_SCOPES.FULL);
  scopeRegistry.registerScope('drive', DRIVE_SCOPES.READONLY);
  scopeRegistry.registerScope('drive', DRIVE_SCOPES.FILE);
  
  // Register feature-specific scopes
  scopeRegistry.registerScope('drive', DRIVE_SCOPES.METADATA);
  scopeRegistry.registerScope('drive', DRIVE_SCOPES.APPDATA);
  
  // Verify all scopes are registered
  const registeredScopes = scopeRegistry.getAllScopes();
  const requiredScopes = Object.values(DRIVE_SCOPES);
  
  const missingScopes = requiredScopes.filter(scope => !registeredScopes.includes(scope));
  if (missingScopes.length > 0) {
    throw new Error(`Failed to register Drive scopes: ${missingScopes.join(', ')}`);
  }
}

export function getDriveScopes(): string[] {
  return Object.values(DRIVE_SCOPES);
}

export function validateDriveScopes(scopes: string[]): boolean {
  const validScopes = new Set(getDriveScopes());
  return scopes.every(scope => validScopes.has(scope));
}

```

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

```typescript
/**
 * Calendar Module Entry Point
 * 
 * This module provides Google Calendar integration following the same singleton
 * pattern as the Gmail module. It handles:
 * - Module initialization with OAuth setup
 * - Calendar service instance management
 * - Type and error exports
 * 
 * Usage:
 * ```typescript
 * // Initialize the module
 * await initializeCalendarModule();
 * 
 * // Get service instance
 * const calendarService = getCalendarService();
 * 
 * // Use calendar operations
 * const events = await calendarService.getEvents({
 *   email: '[email protected]',
 *   maxResults: 10
 * });
 * ```
 */

import { CalendarService } from './service.js';
import {
  GetEventsParams,
  CreateEventParams,
  EventResponse,
  CreateEventResponse,
  CalendarError,
  CalendarModuleConfig
} from './types.js';

// Create singleton instance
let calendarService: CalendarService | null = null;

/**
 * Initialize the Calendar module
 * This must be called before using any calendar operations
 * 
 * @param config - Optional configuration including OAuth scope overrides
 * @returns Initialized CalendarService instance
 * 
 * Note: This function ensures only one instance of the service exists,
 * following the singleton pattern for consistent state management.
 */
export async function initializeCalendarModule(config?: CalendarModuleConfig): Promise<CalendarService> {
  if (!calendarService) {
    calendarService = new CalendarService(config);
    await calendarService.initialize();
  }
  return calendarService;
}

/**
 * Get the initialized Calendar service instance
 * 
 * @returns CalendarService instance
 * @throws CalendarError if the module hasn't been initialized
 * 
 * Note: Always call initializeCalendarModule before using this function
 */
export function getCalendarService(): CalendarService {
  if (!calendarService) {
    throw new CalendarError(
      'Calendar module not initialized',
      'MODULE_NOT_INITIALIZED',
      'Call initializeCalendarModule before using the Calendar service'
    );
  }
  return calendarService;
}

export {
  CalendarService,
  GetEventsParams,
  CreateEventParams,
  EventResponse,
  CreateEventResponse,
  CalendarError,
  CalendarModuleConfig
};

```

--------------------------------------------------------------------------------
/cline_docs/systemPatterns.md:
--------------------------------------------------------------------------------

```markdown
# System Patterns

## Architecture
The system follows a modular architecture with clear separation of concerns:

### Core Components
1. **Scope Registry** (src/modules/tools/scope-registry.ts)
   - Simple scope collection system
   - Gathers required scopes at startup
   - Used only for initial auth setup

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

3. **Account Module** (src/modules/accounts/*)
   - OAuth Client: Implements Google OAuth 2.0 flow
   - Token Manager: Handles token lifecycle
   - Account Manager: Manages account configurations

4. **Service Modules**
   - Gmail Module: Implements email operations
   - Calendar Module: Handles calendar operations

## Key Technical Decisions
1. **Simplest Viable Design**
   - Minimize complexity in permission structures
   - Handle auth through HTTP response codes (401/403)
   - Move OAuth mechanics into platform infrastructure
   - Present simple verb-noun interfaces

2. **Tool Registration Pattern**
   - Tools must be registered in both ListToolsRequestSchema and CallToolRequestSchema
   - Follows verb-noun naming convention (e.g., list_workspace_accounts)

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

## Project Structure
```
src/
├── index.ts                 # MCP server implementation
├── modules/
│   ├── accounts/           # Account & auth handling
│   │   ├── index.ts       # Module entry point
│   │   ├── manager.ts     # Account management
│   │   ├── oauth.ts       # OAuth implementation
│   │   └── token.ts       # Token handling
│   └── gmail/             # Gmail implementation
│       ├── index.ts       # Module entry point
│       ├── service.ts     # Gmail operations
│       └── types.ts       # Gmail types
└── scripts/
    └── setup-google-env.ts # Setup utilities
```

## Configuration Patterns
- Environment-based file paths
- Separate credential storage
- Account configuration management
- Token persistence handling
- Security through proper credential and token management

```

--------------------------------------------------------------------------------
/src/modules/attachments/response-transformer.ts:
--------------------------------------------------------------------------------

```typescript
import { AttachmentIndexService } from './index-service.js';

/**
 * Simplified attachment information visible to AI
 */
export interface AttachmentInfo {
  name: string;
}

/**
 * Service for transforming API responses to hide complex attachment IDs from AI
 */
export class AttachmentResponseTransformer {
  constructor(private indexService: AttachmentIndexService) {}

  /**
   * Transform a response object that may contain attachments
   * Works with both email and calendar responses
   */
  /**
   * Transform a response object that may contain attachments
   * Works with both email and calendar responses
   */
  transformResponse<T>(response: T): T {
    if (Array.isArray(response)) {
      return response.map(item => this.transformResponse(item)) as unknown as T;
    }

    if (typeof response !== 'object' || response === null) {
      return response;
    }

    // Deep clone to avoid modifying original
    const transformed = { ...response } as Record<string, any>;

    // Transform attachments if present
    if ('attachments' in transformed && 
        Array.isArray(transformed.attachments) && 
        'id' in transformed) {
      const messageId = transformed.id as string;
      
      // Store full metadata in index
      transformed.attachments.forEach((attachment: any) => {
        if (attachment?.id && attachment?.name) {
          this.indexService.addAttachment(messageId, {
            id: attachment.id,
            name: attachment.name,
            mimeType: attachment.mimeType || 'application/octet-stream',
            size: attachment.size || 0
          });
        }
      });

      // Replace with simplified version
      transformed.attachments = transformed.attachments.map((attachment: any) => ({
        name: attachment?.name || 'Unknown file'
      }));
    }

    // Recursively transform nested objects
    Object.keys(transformed).forEach(key => {
      if (typeof transformed[key] === 'object' && transformed[key] !== null) {
        transformed[key] = this.transformResponse(transformed[key]);
      }
    });

    return transformed as unknown as T;
  }

  /**
   * Create a refresh placeholder for expired attachments
   */
  static createRefreshPlaceholder(): AttachmentInfo[] {
    return [{
      name: "Attachments expired - Request message again to view"
    }];
  }
}

```

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

```typescript
import { scopeRegistry } from '../tools/scope-registry.js';

// Define Calendar scopes as constants for reuse and testing
// Reference: https://developers.google.com/calendar/api/auth
export const CALENDAR_SCOPES = {
  // Core functionality scopes
  READONLY: 'https://www.googleapis.com/auth/calendar.readonly',  // Required for reading calendars and events
  EVENTS: 'https://www.googleapis.com/auth/calendar.events',      // Required for creating/updating events
  EVENTS_READONLY: 'https://www.googleapis.com/auth/calendar.events.readonly',  // Required for reading events only
  
  // Settings scopes
  SETTINGS_READONLY: 'https://www.googleapis.com/auth/calendar.settings.readonly',  // Required for reading calendar settings
  
  // Full access scope (includes all above permissions)
  FULL_ACCESS: 'https://www.googleapis.com/auth/calendar'         // Complete calendar access
};

/**
 * Register Calendar OAuth scopes at startup.
 * Auth issues will be handled via 401 responses rather than pre-validation.
 * 
 * IMPORTANT: The order of scope registration matters for auth URL generation.
 * Core functionality scopes (readonly) should be registered first,
 * followed by feature-specific scopes (events), and settings scopes last.
 */
export function registerCalendarScopes() {
  // Register core functionality scopes first (order matters for auth URL generation)
  scopeRegistry.registerScope('calendar', CALENDAR_SCOPES.READONLY);   // For reading calendars and events
  scopeRegistry.registerScope('calendar', CALENDAR_SCOPES.EVENTS);     // For managing calendar events
  scopeRegistry.registerScope('calendar', CALENDAR_SCOPES.EVENTS_READONLY);  // For reading events only
  
  // Register settings scopes
  scopeRegistry.registerScope('calendar', CALENDAR_SCOPES.SETTINGS_READONLY);  // For reading calendar settings
  
  // Register full access scope last
  scopeRegistry.registerScope('calendar', CALENDAR_SCOPES.FULL_ACCESS);  // Complete calendar access (includes all above)
  
  // Verify all scopes are registered
  const registeredScopes = scopeRegistry.getAllScopes();
  const requiredScopes = Object.values(CALENDAR_SCOPES);
  
  const missingScopes = requiredScopes.filter(scope => !registeredScopes.includes(scope));
  if (missingScopes.length > 0) {
    throw new Error(`Failed to register Calendar scopes: ${missingScopes.join(', ')}`);
  }
}

```

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

```typescript
import { AttachmentMetadata } from '../attachments/types.js';
import { AttachmentInfo } from '../attachments/response-transformer.js';

export interface CalendarModuleConfig {
  maxAttachmentSize?: number;
  allowedAttachmentTypes?: string[];
}

export interface CalendarAttachment {
  content: string;      // Base64 content
  title: string;       // Filename
  mimeType: string;    // MIME type
  size?: number;       // Size in bytes
}

export interface EventTime {
  dateTime: string;
  timeZone?: string;
}

export interface EventAttendee {
  email: string;
  responseStatus?: string;
}

export interface EventOrganizer {
  email: string;
  self: boolean;
}

export interface EventResponse {
  id: string;
  summary: string;
  description?: string;
  start: EventTime;
  end: EventTime;
  attendees?: EventAttendee[];
  organizer?: EventOrganizer;
  attachments?: AttachmentInfo[];
}

export interface GetEventsParams {
  email: string;
  query?: string;
  maxResults?: number;
  timeMin?: string;
  timeMax?: string;
}

export interface CreateEventParams {
  email: string;
  summary: string;
  description?: string;
  start: EventTime;
  end: EventTime;
  attendees?: {
    email: string;
  }[];
  attachments?: {
    driveFileId?: string;  // For existing Drive files
    content?: string;      // Base64 content for new files
    name: string;
    mimeType: string;
    size?: number;
  }[];
}

export interface CreateEventResponse {
  id: string;
  summary: string;
  htmlLink: string;
  attachments?: AttachmentMetadata[];
}

export interface ManageEventParams {
  email: string;
  eventId: string;
  action: 'accept' | 'decline' | 'tentative' | 'propose_new_time' | 'update_time';
  comment?: string;
  newTimes?: {
    start: EventTime;
    end: EventTime;
  }[];
}

export interface ManageEventResponse {
  success: boolean;
  eventId: string;
  action: string;
  status: 'completed' | 'proposed' | 'updated';
  htmlLink?: string;
  proposedTimes?: {
    start: EventTime;
    end: EventTime;
  }[];
}

export interface DeleteEventParams {
  email: string;
  eventId: string;
  sendUpdates?: 'all' | 'externalOnly' | 'none';
  deletionScope?: 'entire_series' | 'this_and_following';
}

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

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

```

--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------

```typescript
import { appendFileSync, existsSync, mkdirSync } from 'fs';
import { dirname } from 'path';

/**
 * Logger utility with configurable logging behavior.
 * 
 * Supports two logging modes:
 * - normal: Uses appropriate console methods for each log level (error, warn, info, debug)
 * - strict: Routes all non-JSON-RPC messages to stderr for compatibility with tools like Claude desktop
 * 
 * Configure via:
 * - LOG_MODE environment variable:
 *   - LOG_MODE=normal (default) - Standard logging behavior
 *   - LOG_MODE=strict - All logs except JSON-RPC go to stderr
 * - LOG_FILE environment variable:
 *   - If set, logs will also be written to this file
 * 
 * For testing: The logger should be mocked in tests to prevent console noise.
 * See src/__helpers__/testSetup.ts for the mock implementation.
 */

type LogMode = 'normal' | 'strict';

const LOG_MODE = (process.env.LOG_MODE || 'normal') as LogMode;
const LOG_FILE = process.env.LOG_FILE;

// Ensure log directory exists if LOG_FILE is set
if (LOG_FILE) {
  const dir = dirname(LOG_FILE);
  if (!existsSync(dir)) {
    mkdirSync(dir, { recursive: true });
  }
}

const isJsonRpc = (msg: any): boolean => {
  if (typeof msg !== 'string') return false;
  return msg.startsWith('{"jsonrpc":') || msg.startsWith('{"id":');
};

const writeToLogFile = (level: string, ...args: any[]) => {
  if (!LOG_FILE) return;
  try {
    const timestamp = new Date().toISOString();
    const message = args.map(arg => 
      typeof arg === 'string' ? arg : JSON.stringify(arg)
    ).join(' ');
    appendFileSync(LOG_FILE, `[${timestamp}] [${level.toUpperCase()}] ${message}\n`);
  } catch (error) {
    console.error('Failed to write to log file:', error);
  }
};

const logger = {
  error: (...args: any[]) => {
    console.error(...args);
    writeToLogFile('error', ...args);
  },
  warn: (...args: any[]) => {
    if (LOG_MODE === 'strict' || isJsonRpc(args[0])) {
      console.error(...args);
    } else {
      console.warn(...args);
    }
    writeToLogFile('warn', ...args);
  },
  info: (...args: any[]) => {
    if (LOG_MODE === 'strict' || isJsonRpc(args[0])) {
      console.error(...args);
    } else {
      console.info(...args);
    }
    writeToLogFile('info', ...args);
  },
  debug: (...args: any[]) => {
    if (!process.env.DEBUG) return;
    if (LOG_MODE === 'strict' || isJsonRpc(args[0])) {
      console.error(...args);
    } else {
      console.debug(...args);
    }
    if (process.env.DEBUG) {
      writeToLogFile('debug', ...args);
    }
  }
};

export default logger;

```

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

```typescript
import { registerCalendarScopes, CALENDAR_SCOPES } from '../scopes.js';
import { scopeRegistry } from '../../tools/scope-registry.js';

describe('Calendar Scopes', () => {
  beforeEach(() => {
    // Reset the scope registry before each test
    // @ts-expect-error - accessing private property for testing
    scopeRegistry.scopes = new Map();
    // @ts-expect-error - accessing private property for testing
    scopeRegistry.scopeOrder = [];
  });

  it('should register all required Calendar scopes', () => {
    registerCalendarScopes();
    const registeredScopes = scopeRegistry.getAllScopes();
    
    // Required scopes for Calendar functionality
    const requiredScopes = [
      CALENDAR_SCOPES.READONLY,         // For viewing events
      CALENDAR_SCOPES.EVENTS,           // For creating/modifying events
      CALENDAR_SCOPES.SETTINGS_READONLY, // For calendar settings
      CALENDAR_SCOPES.FULL_ACCESS       // For full calendar management
    ];

    // Verify each required scope is registered
    requiredScopes.forEach(scope => {
      expect(registeredScopes).toContain(scope);
    });
  });

  it('should register scopes in correct order', () => {
    registerCalendarScopes();
    const registeredScopes = scopeRegistry.getAllScopes();

    // Core functionality scopes should come first
    const coreScopes = [
      CALENDAR_SCOPES.READONLY
    ];

    // Feature-specific scopes should come next
    const featureScopes = [
      CALENDAR_SCOPES.EVENTS,
      CALENDAR_SCOPES.FULL_ACCESS
    ];

    // Settings scopes should come last
    const settingsScopes = [
      CALENDAR_SCOPES.SETTINGS_READONLY
    ];

    // Verify order of scope groups
    const firstCoreIndex = Math.min(...coreScopes.map(scope => registeredScopes.indexOf(scope)));
    const firstFeatureIndex = Math.min(...featureScopes.map(scope => registeredScopes.indexOf(scope)));
    const firstSettingsIndex = Math.min(...settingsScopes.map(scope => registeredScopes.indexOf(scope)));

    expect(firstCoreIndex).toBeLessThan(firstFeatureIndex);
    expect(firstFeatureIndex).toBeLessThan(firstSettingsIndex);
  });

  it('should maintain scope registration when re-registering', () => {
    // Register scopes first time
    registerCalendarScopes();
    const initialScopes = scopeRegistry.getAllScopes();

    // Register scopes second time
    registerCalendarScopes();
    const finalScopes = scopeRegistry.getAllScopes();

    // Verify all scopes are still registered in same order
    expect(finalScopes).toEqual(initialScopes);
  });
});

```

--------------------------------------------------------------------------------
/.github/workflows/docker-publish.yml:
--------------------------------------------------------------------------------

```yaml
name: Test, Build and Deploy

on:
  push:
    branches: [ main, fix/* ]
    tags: [ 'v*' ]
  pull_request:
    branches: [ main ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
  NODE_VERSION: '20'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Type check
        run: npm run type-check

      - name: Lint
        run: npm run lint

      - name: Run tests
        run: npm test

  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Log in to the Container registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=raw,value=latest,enable={{is_default_branch}}
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=sha,format=long

      - name: Build and push Docker image
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64
          build-args: |
            DOCKER_HASH=${{ github.sha }}

  cleanup:
    name: Cleanup
    needs: build-and-deploy
    runs-on: ubuntu-latest
    
    steps:
      - name: Remove old packages
        uses: actions/delete-package-versions@v4
        with:
          package-name: ${{ github.event.repository.name }}
          package-type: container
          min-versions-to-keep: 10
          delete-only-untagged-versions: true

```

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

```typescript
import { registerGmailScopes, GMAIL_SCOPES } from '../scopes.js';
import { scopeRegistry } from '../../tools/scope-registry.js';

describe('Gmail Scopes', () => {
  beforeEach(() => {
    // Reset the scope registry before each test
    // @ts-expect-error - accessing private property for testing
    scopeRegistry.scopes = new Map();
    // @ts-expect-error - accessing private property for testing
    scopeRegistry.scopeOrder = [];
  });

  it('should register all required Gmail scopes', () => {
    registerGmailScopes();
    const registeredScopes = scopeRegistry.getAllScopes();
    
    // Required scopes for Gmail functionality
    const requiredScopes = [
      GMAIL_SCOPES.READONLY,  // For reading emails and labels
      GMAIL_SCOPES.SEND,      // For sending emails
      GMAIL_SCOPES.MODIFY,    // For modifying emails and drafts
      GMAIL_SCOPES.LABELS,    // For label management
      GMAIL_SCOPES.SETTINGS_BASIC,    // For Gmail settings
      GMAIL_SCOPES.SETTINGS_SHARING   // For settings management
    ];

    // Verify each required scope is registered
    requiredScopes.forEach(scope => {
      expect(registeredScopes).toContain(scope);
    });
  });

  it('should register scopes in correct order', () => {
    registerGmailScopes();
    const registeredScopes = scopeRegistry.getAllScopes();

    // Core functionality scopes should come first
    const coreScopes = [
      GMAIL_SCOPES.READONLY,
      GMAIL_SCOPES.SEND,
      GMAIL_SCOPES.MODIFY
    ];

    // Feature-specific scopes should come next
    const featureScopes = [
      GMAIL_SCOPES.LABELS
    ];

    // Settings scopes should come last
    const settingsScopes = [
      GMAIL_SCOPES.SETTINGS_BASIC,
      GMAIL_SCOPES.SETTINGS_SHARING
    ];

    // Verify order of scope groups
    const firstCoreIndex = Math.min(...coreScopes.map(scope => registeredScopes.indexOf(scope)));
    const firstFeatureIndex = Math.min(...featureScopes.map(scope => registeredScopes.indexOf(scope)));
    const firstSettingsIndex = Math.min(...settingsScopes.map(scope => registeredScopes.indexOf(scope)));

    expect(firstCoreIndex).toBeLessThan(firstFeatureIndex);
    expect(firstFeatureIndex).toBeLessThan(firstSettingsIndex);
  });

  it('should maintain scope registration when re-registering', () => {
    // Register scopes first time
    registerGmailScopes();
    const initialScopes = scopeRegistry.getAllScopes();

    // Register scopes second time
    registerGmailScopes();
    const finalScopes = scopeRegistry.getAllScopes();

    // Verify all scopes are still registered in same order
    expect(finalScopes).toEqual(initialScopes);
  });
});

```

--------------------------------------------------------------------------------
/src/api/handler.ts:
--------------------------------------------------------------------------------

```typescript
import { GoogleApiRequestParams, GoogleApiResponse, GoogleApiError } from '../types.js';
import { GoogleApiRequest } from './request.js';
import { EndpointValidator } from './validators/endpoint.js';
import { ParameterValidator } from './validators/parameter.js';
import { AttachmentResponseTransformer } from '../modules/attachments/response-transformer.js';
import { AttachmentIndexService } from '../modules/attachments/index-service.js';

export class RequestHandler {
  private endpointValidator: EndpointValidator;
  private parameterValidator: ParameterValidator;
  private attachmentTransformer: AttachmentResponseTransformer;

  constructor(
    private apiRequest: GoogleApiRequest,
    attachmentIndexService: AttachmentIndexService = AttachmentIndexService.getInstance()
  ) {
    this.endpointValidator = new EndpointValidator();
    this.parameterValidator = new ParameterValidator();
    this.attachmentTransformer = new AttachmentResponseTransformer(attachmentIndexService);
  }

  async handleRequest(params: GoogleApiRequestParams, token: string): Promise<GoogleApiResponse> {
    try {
      // Step 1: Basic validation
      await this.validateRequest(params);

      // Step 2: Execute request
      const result = await this.executeRequest(params, token);

      // Step 3: Format response
      return this.formatSuccessResponse(result);
    } catch (error) {
      return this.formatErrorResponse(error);
    }
  }

  private async validateRequest(params: GoogleApiRequestParams): Promise<void> {
    // Validate endpoint format and availability
    await this.endpointValidator.validate(params.api_endpoint);

    // Validate required parameters
    await this.parameterValidator.validate(params);
  }

  private async executeRequest(params: GoogleApiRequestParams, token: string): Promise<any> {
    return this.apiRequest.makeRequest({
      endpoint: params.api_endpoint,
      method: params.method,
      params: params.params,
      token
    });
  }

  private formatSuccessResponse(data: any): GoogleApiResponse {
    // Transform response to simplify attachments for AI
    const transformedData = this.attachmentTransformer.transformResponse(data);
    
    return {
      status: 'success',
      data: transformedData
    };
  }

  private formatErrorResponse(error: unknown): GoogleApiResponse {
    if (error instanceof GoogleApiError) {
      return {
        status: 'error',
        error: error.message,
        resolution: error.resolution
      };
    }

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

```

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

```typescript
import {
  GetContactsParams,
  GetContactsResponse,
  ContactsError
} from "../modules/contacts/types.js";
import { ContactsService } from "../services/contacts/index.js";
import { validateEmail } from "../utils/account.js";
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
import { getAccountManager } from "../modules/accounts/index.js";

// Singleton instances - Initialize or inject as per project pattern
let contactsService: ContactsService;
let accountManager: ReturnType<typeof getAccountManager>;

/**
 * Initialize required services.
 * This should likely be integrated into a central initialization process.
 */
async function initializeServices() {
  if (!contactsService) {
    // Assuming ContactsService has a static getInstance or similar
    // or needs to be instantiated here. Using direct instantiation for now.
    contactsService = new ContactsService();
    // If ContactsService requires async initialization await it here.
    // await contactsService.initialize();
  }

  if (!accountManager) {
    accountManager = getAccountManager();
  }
}

/**
 * Handler function for retrieving Google Contacts.
 */
export async function handleGetContacts(
  params: GetContactsParams
): Promise<GetContactsResponse> {
  await initializeServices(); // Ensure services are ready
  const { email, personFields, pageSize, pageToken } = params;

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

  if (!personFields) {
    throw new McpError(
      ErrorCode.InvalidParams,
      'personFields parameter is required (e.g. "names,emailAddresses")'
    );
  }

  // Use accountManager for token renewal like in Gmail handlers
  return accountManager.withTokenRenewal(email, async () => {
    try {
      const result = await contactsService.getContacts({
        email,
        personFields,
        pageSize,
        pageToken
      });
      return result;
    } catch (error) {
      if (error instanceof ContactsError) {
        // Map ContactsError to McpError
        throw new McpError(
          ErrorCode.InternalError, // Or map specific error codes
          `Contacts API Error: ${error.message}`,
          { code: error.code, details: error.details }
        );
      } else if (error instanceof McpError) {
        // Re-throw existing McpErrors (like auth errors from token renewal)
        throw error;
      } else {
        // Catch unexpected errors
        throw new McpError(
          ErrorCode.InternalError,
          `Failed to get contacts: ${
            error instanceof Error ? error.message : "Unknown error"
          }`
        );
      }
    }
  });
}

// Add other handlers like handleSearchContacts later

```

--------------------------------------------------------------------------------
/src/modules/tools/__tests__/scope-registry.test.ts:
--------------------------------------------------------------------------------

```typescript
import { scopeRegistry } from '../scope-registry.js';

describe('ScopeRegistry', () => {
  beforeEach(() => {
    // Reset the scope registry before each test
    // @ts-expect-error - accessing private property for testing
    scopeRegistry.scopes = new Map();
    // @ts-expect-error - accessing private property for testing
    scopeRegistry.scopeOrder = [];
  });

  it('should maintain scope registration order', () => {
    const scopes = [
      'https://www.googleapis.com/auth/gmail.readonly',
      'https://www.googleapis.com/auth/gmail.send',
      'https://www.googleapis.com/auth/gmail.modify',
      'https://www.googleapis.com/auth/gmail.labels'
    ];

    // Register scopes
    scopes.forEach(scope => {
      scopeRegistry.registerScope('gmail', scope);
    });

    // Verify order matches registration order
    expect(scopeRegistry.getAllScopes()).toEqual(scopes);
  });

  it('should update scope position when re-registered', () => {
    const scope1 = 'https://www.googleapis.com/auth/gmail.readonly';
    const scope2 = 'https://www.googleapis.com/auth/gmail.send';
    const scope3 = 'https://www.googleapis.com/auth/gmail.labels';

    // Register initial scopes
    scopeRegistry.registerScope('gmail', scope1);
    scopeRegistry.registerScope('gmail', scope2);
    scopeRegistry.registerScope('gmail', scope3);

    // Re-register scope1 (should move to end)
    scopeRegistry.registerScope('gmail', scope1);

    // Verify new order
    expect(scopeRegistry.getAllScopes()).toEqual([
      scope2,
      scope3,
      scope1
    ]);
  });

  it('should maintain tool associations when re-registering scopes', () => {
    const scope = 'https://www.googleapis.com/auth/gmail.labels';
    
    // Register with first tool
    scopeRegistry.registerScope('tool1', scope);
    
    // Re-register with second tool
    scopeRegistry.registerScope('tool2', scope);
    
    // Get scopes for both tools
    const tool1Scopes = scopeRegistry.getToolScopes('tool1');
    const tool2Scopes = scopeRegistry.getToolScopes('tool2');
    
    // Verify scope is associated with latest tool only
    expect(tool1Scopes).not.toContain(scope);
    expect(tool2Scopes).toContain(scope);
  });

  it('should return empty array for non-existent tool', () => {
    const scopes = scopeRegistry.getToolScopes('non-existent-tool');
    expect(scopes).toEqual([]);
  });

  it('should handle multiple scopes for same tool', () => {
    const tool = 'gmail';
    const scopes = [
      'https://www.googleapis.com/auth/gmail.readonly',
      'https://www.googleapis.com/auth/gmail.send',
      'https://www.googleapis.com/auth/gmail.labels'
    ];

    scopes.forEach(scope => {
      scopeRegistry.registerScope(tool, scope);
    });

    const toolScopes = scopeRegistry.getToolScopes(tool);
    expect(toolScopes).toEqual(scopes);
  });
});

```

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

```typescript
import { SearchCriteria } from '../types.js';

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

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

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

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

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

    // Handle date range (use Gmail's date format: YYYY/MM/DD)
    if (criteria.after) {
      const afterDate = new Date(criteria.after);
      const afterStr = `${afterDate.getFullYear()}/${(afterDate.getMonth() + 1).toString().padStart(2, '0')}/${afterDate.getDate().toString().padStart(2, '0')}`;
      queryParts.push(`after:${afterStr}`);
    }
    if (criteria.before) {
      const beforeDate = new Date(criteria.before);
      const beforeStr = `${beforeDate.getFullYear()}/${(beforeDate.getMonth() + 1).toString().padStart(2, '0')}/${beforeDate.getDate().toString().padStart(2, '0')}`;
      queryParts.push(`before:${beforeStr}`);
    }

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

    // Handle labels (no need to join with spaces, Gmail supports multiple label: operators)
    if (criteria.labels && criteria.labels.length > 0) {
      criteria.labels.forEach(label => {
        queryParts.push(`label:${label}`);
      });
    }

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

    // Handle spam/trash inclusion
    if (criteria.includeSpam) {
      queryParts.push('in:anywhere');
    }

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

    return queryParts.join(' ');
  }
}

```

--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------

```markdown
# Attachment System Refactor

## Completed
- Created AttachmentIndexService
  - Map-based storage using messageId + filename as key
  - Built-in size limit (256 entries)
  - Expiry handling (1 hour timeout)

- Created AttachmentResponseTransformer
  - Simplifies attachment info for AI (filename only)
  - Integrated into API layer

- Updated Services
  - Gmail attachment handling now uses simplified format
  - Calendar attachment handling now uses simplified format

- Added Cleanup System
  - Created AttachmentCleanupService
  - Cleanup triggers on:
    - Index reaching size limit
    - Retrieving expired attachments

## Implementation Status

### Completed ✓
1. Core Components
   - AttachmentIndexService with map-based storage
   - Size limit (256 entries) implementation
   - Expiry handling (1 hour timeout)
   - Filename + messageId based lookup

2. Response Transformation
   - AttachmentResponseTransformer implementation
   - Unified handling for email and calendar attachments
   - Simplified format for AI (filename only)
   - Full metadata preservation internally

3. Service Integration
   - Gmail attachment handling
   - Calendar attachment handling
   - Abstracted attachment interface

4. Test Infrastructure
   - Basic test suite setup
   - Core functionality tests
   - Integration test structure

### Completed ✓
1. Testing Fixes
   - ✓ Simplified test suite to focus on core functionality
   - ✓ Removed complex timing-dependent tests
   - ✓ Added basic service operation tests
   - ✓ Verified cleanup service functionality
   - ✓ Fixed Drive service test timing issues

2. Cleanup System Refinements
   - ✓ Immediate cleanup on service start
   - ✓ Activity-based interval adjustments
   - ✓ Performance monitoring accuracy

### Version 1.1 Changes ✓
1. Attachment System Improvements
   - ✓ Simplified attachment data in responses (filename only)
   - ✓ Maintained full metadata in index service
   - ✓ Verified download functionality with simplified format
   - ✓ Updated documentation and architecture

### Next Steps 📋
1. Documentation
   - [x] Add inline documentation
   - [x] Update API documentation
   - [x] Add usage examples

## Example Transformation
Before:
```json
{
  "id": "1952a804b3a15f6a",
  "attachments": [{
    "id": "ANGjdJ9gKpYkZ5NRp80mRJVCUe9XsAB93LHl22UrPU-9-pBPadGczuK3...",
    "name": "document.pdf",
    "mimeType": "application/pdf",
    "size": 1234
  }]
}
```

After:
```json
{
  "id": "1952a804b3a15f6a",
  "attachments": [{
    "name": "document.pdf"
  }]
}

### Future Improvements 🚀
1. Performance Optimizations
   - [ ] Implement batch processing for large attachment sets
   - [ ] Add caching layer for frequently accessed attachments
   - [ ] Optimize cleanup intervals based on usage patterns

2. Enhanced Features
   - [ ] Support for streaming large attachments
   - [ ] Add compression options for storage
   - [ ] Implement selective metadata retention

```

--------------------------------------------------------------------------------
/src/scripts/setup-environment.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = path.resolve(__dirname, '../..');

interface SetupCheck {
  path: string;
  type: 'file' | 'directory';
  example?: string;
  required: boolean;
}

const REQUIRED_ITEMS: SetupCheck[] = [
  {
    path: 'config',
    type: 'directory',
    required: true
  },
  {
    path: 'config/credentials',
    type: 'directory',
    required: true
  },
  {
    path: 'config/gauth.json',
    type: 'file',
    example: 'config/gauth.example.json',
    required: true
  },
  {
    path: 'config/accounts.json',
    type: 'file',
    example: 'config/accounts.example.json',
    required: true
  }
];

async function fileExists(path: string): Promise<boolean> {
  try {
    await fs.access(path);
    return true;
  } catch {
    return false;
  }
}

async function setupEnvironment(): Promise<void> {
  console.log('\n🔧 Setting up Google Workspace MCP Server environment...\n');
  
  for (const item of REQUIRED_ITEMS) {
    const fullPath = path.join(ROOT_DIR, item.path);
    const exists = await fileExists(fullPath);
    
    if (!exists) {
      if (item.type === 'directory') {
        console.log(`📁 Creating directory: ${item.path}`);
        await fs.mkdir(fullPath, { recursive: true });
      } else if (item.example) {
        const examplePath = path.join(ROOT_DIR, item.example);
        const exampleExists = await fileExists(examplePath);
        
        if (exampleExists) {
          console.log(`📄 Creating ${item.path} from example`);
          await fs.copyFile(examplePath, fullPath);
        } else if (item.required) {
          console.error(`❌ Error: Example file ${item.example} not found`);
          process.exit(1);
        }
      } else if (item.required) {
        console.error(`❌ Error: Required ${item.type} ${item.path} is missing`);
        process.exit(1);
      }
    } else {
      console.log(`✅ ${item.path} already exists`);
    }
  }

  console.log('\n✨ Environment setup complete!\n');
  console.log('Next steps:');
  console.log('1. Configure OAuth credentials in config/gauth.json');
  console.log('   - Create a project in Google Cloud Console');
  console.log('   - Enable Gmail API');
  console.log('   - Configure OAuth consent screen');
  console.log('   - Create OAuth 2.0 credentials');
  console.log('   - Copy credentials to config/gauth.json');
  console.log('\n2. Configure accounts in config/accounts.json');
  console.log('   - Add Google accounts you want to use');
  console.log('   - Set appropriate categories and descriptions');
  console.log('\n3. Run authentication for each account:');
  console.log('   ```');
  console.log('   npx ts-node src/scripts/setup-google-env.ts');
  console.log('   ```');
}

setupEnvironment().catch(error => {
  console.error('\n❌ Setup failed:', error);
  process.exit(1);
});

```

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

```typescript
/**
 * Service for managing attachment metadata and providing simplified references for AI interactions
 */

export interface AttachmentMetadataInternal {
  messageId: string;
  filename: string;
  originalId: string;
  mimeType: string;
  size: number;
  timestamp: number;
}

export class AttachmentIndexService {
  private static instance: AttachmentIndexService;
  private index: Map<string, AttachmentMetadataInternal>;
  private readonly _maxEntries: number = 256;
  private expiryMs: number = 3600000; // 1 hour

  /**
   * Get the maximum number of entries allowed in the index
   */
  get maxEntries(): number {
    return this._maxEntries;
  }

  private constructor() {
    this.index = new Map();
  }

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

  /**
   * Add or update attachment metadata in the index
   */
  addAttachment(messageId: string, attachment: {
    id: string;
    name: string;
    mimeType: string;
    size: number;
  }): void {
    // Clean expired entries if we're near capacity
    if (this.index.size >= this._maxEntries) {
      this.cleanExpiredEntries();
      
      // If still at capacity after cleaning expired entries,
      // remove oldest entries until we have space
      if (this.index.size >= this._maxEntries) {
        const entries = Array.from(this.index.entries())
          .sort(([, a], [, b]) => a.timestamp - b.timestamp);
          
        while (this.index.size >= this._maxEntries && entries.length > 0) {
          const [key] = entries.shift()!;
          this.index.delete(key);
        }
      }
    }

    const key = `${messageId}_${attachment.name}`;
    this.index.set(key, {
      messageId,
      filename: attachment.name,
      originalId: attachment.id,
      mimeType: attachment.mimeType,
      size: attachment.size,
      timestamp: Date.now()
    });
  }

  /**
   * Retrieve attachment metadata by message ID and filename
   */
  getMetadata(messageId: string, filename: string): AttachmentMetadataInternal | undefined {
    const key = `${messageId}_${filename}`;
    const metadata = this.index.get(key);

    if (metadata && this.isExpired(metadata)) {
      this.index.delete(key);
      return undefined;
    }

    return metadata;
  }

  /**
   * Remove expired entries from the index
   */
  public cleanExpiredEntries(): void {
    const now = Date.now();
    for (const [key, metadata] of this.index.entries()) {
      if (this.isExpired(metadata)) {
        this.index.delete(key);
      }
    }
  }

  /**
   * Check if an entry has expired
   */
  private isExpired(metadata: AttachmentMetadataInternal): boolean {
    return Date.now() - metadata.timestamp > this.expiryMs;
  }

  /**
   * Get current number of entries in the index
   */
  get size(): number {
    return this.index.size;
  }
}

```

--------------------------------------------------------------------------------
/src/scripts/setup-google-env.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = path.resolve(__dirname, '../..');

async function setupGoogleEnv(): Promise<void> {
  try {
    // Ensure credentials directory exists
    const credentialsDir = path.join(ROOT_DIR, 'config', 'credentials');
    try {
      await fs.access(credentialsDir);
    } catch {
      await fs.mkdir(credentialsDir, { recursive: true });
    }

    // Read and encode the credentials file
    const gauthPath = path.join(ROOT_DIR, 'config', 'gauth.json');
    const credentials = await fs.readFile(gauthPath, 'utf8');
    const credentialsBase64 = Buffer.from(credentials).toString('base64');

    // Read and encode any existing tokens
    const tokenFiles = await fs.readdir(credentialsDir);
    const tokenData: Record<string, string> = {};

    for (const file of tokenFiles) {
      if (file.endsWith('.token.json')) {
        const email = file.replace('.token.json', '').replace(/-/g, '.');
        const tokenPath = path.join(credentialsDir, file);
        const token = await fs.readFile(tokenPath, 'utf8');
        tokenData[email] = Buffer.from(token).toString('base64');
      }
    }

    // Read existing .env if it exists
    const envPath = path.join(ROOT_DIR, '.env');
    let envContent = '';
    try {
      envContent = await fs.readFile(envPath, 'utf8');
      // Remove any existing Google credentials
      envContent = envContent
        .split('\n')
        .filter(line => !line.startsWith('GOOGLE_'))
        .join('\n');
      if (envContent && !envContent.endsWith('\n')) {
        envContent += '\n';
      }
    } catch {
      // If .env doesn't exist, start with empty content
    }

    // Add the new credentials
    envContent += `GOOGLE_CREDENTIALS=${credentialsBase64}\n`;
    
    // Add tokens for each account
    for (const [email, token] of Object.entries(tokenData)) {
      const safeEmail = email.replace(/[@.]/g, '_').toUpperCase();
      envContent += `GOOGLE_TOKEN_${safeEmail}=${token}\n`;
    }

    // Write to .env file
    await fs.writeFile(envPath, envContent);

    console.log('\n✅ Successfully configured Google environment:');
    console.log(`- Credentials loaded from: ${gauthPath}`);
    console.log(`- Tokens loaded: ${Object.keys(tokenData).length}`);
    console.log(`- Environment variables written to: ${envPath}`);
    
    if (Object.keys(tokenData).length === 0) {
      console.log('\nℹ️  No tokens found. Run authentication for each account to generate tokens.');
    }

  } catch (error) {
    console.error('\n❌ Setup failed:', error instanceof Error ? error.message : error);
    console.log('\nPlease ensure:');
    console.log('1. config/gauth.json exists with valid Google OAuth credentials');
    console.log('2. You have write permissions for the .env file');
    process.exit(1);
  }
}

setupGoogleEnv().catch(error => {
  console.error('Fatal error:', error);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/src/utils/workspace.ts:
--------------------------------------------------------------------------------

```typescript
import path from 'path';
import fs from 'fs/promises';

export class WorkspaceManager {
  private basePath: string;

  constructor() {
    this.basePath = process.env.WORKSPACE_BASE_PATH || '/app/workspace';
  }

  /**
   * Get the workspace directory path for a specific email account
   */
  private getAccountPath(email: string): string {
    return path.join(this.basePath, email);
  }

  /**
   * Get the downloads directory path for a specific email account
   */
  private getDownloadsPath(email: string): string {
    return path.join(this.getAccountPath(email), 'downloads');
  }

  /**
   * Get the uploads directory path for a specific email account
   */
  private getUploadsPath(email: string): string {
    return path.join(this.getAccountPath(email), 'uploads');
  }

  /**
   * Get the shared temporary directory path
   */
  private getTempPath(): string {
    return path.join(this.basePath, 'shared', 'temp');
  }

  /**
   * Ensure all required directories exist for an email account
   */
  async initializeAccountDirectories(email: string): Promise<void> {
    const dirs = [
      this.getAccountPath(email),
      this.getDownloadsPath(email),
      this.getUploadsPath(email)
    ];

    for (const dir of dirs) {
      await fs.mkdir(dir, { recursive: true, mode: 0o750 });
    }
  }

  /**
   * Generate a path for a downloaded file
   */
  async getDownloadPath(email: string, filename: string): Promise<string> {
    await this.initializeAccountDirectories(email);
    return path.join(this.getDownloadsPath(email), filename);
  }

  /**
   * Generate a path for an upload file
   */
  async getUploadPath(email: string, filename: string): Promise<string> {
    await this.initializeAccountDirectories(email);
    return path.join(this.getUploadsPath(email), filename);
  }

  /**
   * Generate a temporary file path
   */
  async getTempFilePath(prefix: string): Promise<string> {
    const tempDir = this.getTempPath();
    await fs.mkdir(tempDir, { recursive: true, mode: 0o750 });
    const timestamp = new Date().getTime();
    const random = Math.random().toString(36).substring(2, 15);
    return path.join(tempDir, `${prefix}-${timestamp}-${random}`);
  }

  /**
   * Clean up old files in the temporary directory
   * @param maxAge Maximum age in milliseconds before a file is considered old
   */
  async cleanupTempFiles(maxAge: number = 24 * 60 * 60 * 1000): Promise<void> {
    const tempDir = this.getTempPath();
    try {
      const files = await fs.readdir(tempDir);
      const now = Date.now();

      for (const file of files) {
        const filePath = path.join(tempDir, file);
        const stats = await fs.stat(filePath);

        if (now - stats.mtimeMs > maxAge) {
          await fs.unlink(filePath);
        }
      }
    } catch (error: unknown) {
      // Ignore errors if temp directory doesn't exist
      if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
        throw error;
      }
    }
  }
}

// Export singleton instance
export const workspaceManager = new WorkspaceManager();

```

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

```typescript
import { OAuth2Client } from 'google-auth-library';
import { AccountError } from './types.js';
import { OAuthCallbackServer } from './callback-server.js';
import logger from '../../utils/logger.js';

export class GoogleOAuthClient {
  private oauth2Client: OAuth2Client;
  private callbackServer: OAuthCallbackServer;

  constructor() {
    const clientId = process.env.GOOGLE_CLIENT_ID;
    const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
    
    if (!clientId || !clientSecret) {
      throw new AccountError(
        'Missing OAuth credentials',
        'AUTH_CONFIG_ERROR',
        'GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be provided'
      );
    }

    this.callbackServer = OAuthCallbackServer.getInstance();
    
    logger.info('Initializing OAuth client...');
    this.oauth2Client = new OAuth2Client(
      clientId,
      clientSecret,
      this.callbackServer.getCallbackUrl() // Use localhost:8080 instead of OOB
    );
    logger.info('OAuth client initialized successfully');
    
    // Ensure the callback server is running
    this.callbackServer.ensureServerRunning().catch(error => {
      logger.error('Failed to start OAuth callback server:', error);
    });
  }

  getAuthClient(): OAuth2Client {
    return this.oauth2Client;
  }

  /**
   * Generates the OAuth authorization URL
   * IMPORTANT: When using the generated URL, always use it exactly as returned.
   * Do not attempt to modify, reformat, or reconstruct the URL as this can break
   * the authentication flow. The URL contains carefully encoded parameters that
   * must be preserved exactly as provided.
   */
  async generateAuthUrl(scopes: string[]): Promise<string> {
    logger.info('Generating OAuth authorization URL');
    const url = this.oauth2Client.generateAuthUrl({
      access_type: 'offline',
      scope: scopes,
      prompt: 'consent'
    });
    logger.debug('Authorization URL generated successfully');
    return url;
  }

  async waitForAuthorizationCode(): Promise<string> {
    logger.info('Starting OAuth callback server and waiting for authorization...');
    return await this.callbackServer.waitForAuthorizationCode();
  }

  async getTokenFromCode(code: string): Promise<any> {
    logger.info('Exchanging authorization code for tokens');
    try {
      const { tokens } = await this.oauth2Client.getToken(code);
      logger.info('Successfully obtained tokens from auth code');
      return tokens;
    } catch (error) {
      throw new AccountError(
        'Failed to exchange authorization code for tokens',
        'AUTH_CODE_ERROR',
        'Please ensure the authorization code is valid and not expired'
      );
    }
  }

  async refreshToken(refreshToken: string): Promise<any> {
    logger.info('Refreshing access token');
    try {
      this.oauth2Client.setCredentials({
        refresh_token: refreshToken
      });
      const { credentials } = await this.oauth2Client!.refreshAccessToken();
      logger.info('Successfully refreshed access token');
      return credentials;
    } catch (error) {
      throw new AccountError(
        'Failed to refresh token',
        'TOKEN_REFRESH_ERROR',
        'Please re-authenticate the account'
      );
    }
  }
}

```

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

```typescript
/**
 * Gmail's allowed label colors
 * These are the only colors that Gmail's API accepts for label customization
 */
export const GMAIL_LABEL_COLORS = {
  default: {
    textColor: '#000000',
    backgroundColor: '#ffffff'
  },
  red: {
    textColor: '#ffffff',
    backgroundColor: '#dc3545'
  },
  orange: {
    textColor: '#000000',
    backgroundColor: '#ffc107'
  },
  yellow: {
    textColor: '#000000',
    backgroundColor: '#ffeb3b'
  },
  green: {
    textColor: '#ffffff',
    backgroundColor: '#28a745'
  },
  teal: {
    textColor: '#ffffff',
    backgroundColor: '#20c997'
  },
  blue: {
    textColor: '#ffffff',
    backgroundColor: '#007bff'
  },
  purple: {
    textColor: '#ffffff',
    backgroundColor: '#6f42c1'
  },
  pink: {
    textColor: '#ffffff',
    backgroundColor: '#e83e8c'
  },
  gray: {
    textColor: '#ffffff',
    backgroundColor: '#6c757d'
  }
} as const;

export type GmailLabelColor = keyof typeof GMAIL_LABEL_COLORS;

/**
 * Validates if a color combination is allowed by Gmail
 * @param textColor - Hex color code for text
 * @param backgroundColor - Hex color code for background
 * @returns true if the color combination is valid, false otherwise
 */
export function isValidGmailLabelColor(textColor: string, backgroundColor: string): boolean {
  return Object.values(GMAIL_LABEL_COLORS).some(
    color => color.textColor.toLowerCase() === textColor.toLowerCase() &&
             color.backgroundColor.toLowerCase() === backgroundColor.toLowerCase()
  );
}

/**
 * Gets the closest valid Gmail label color based on a hex code
 * @param backgroundColor - Hex color code to match
 * @returns The closest matching valid Gmail label color combination
 */
export function getNearestGmailLabelColor(backgroundColor: string): typeof GMAIL_LABEL_COLORS[GmailLabelColor] {
  // Remove # if present and convert to lowercase
  const targetColor = backgroundColor.replace('#', '').toLowerCase();
  
  // Convert hex to RGB
  const targetRGB = {
    r: parseInt(targetColor.substr(0, 2), 16),
    g: parseInt(targetColor.substr(2, 2), 16),
    b: parseInt(targetColor.substr(4, 2), 16)
  };

  // Calculate distance to each valid color
  let closestColor = 'default' as GmailLabelColor;
  let minDistance = Number.MAX_VALUE;

  Object.entries(GMAIL_LABEL_COLORS).forEach(([name, color]) => {
    const validColor = color.backgroundColor.replace('#', '').toLowerCase();
    const validRGB = {
      r: parseInt(validColor.substr(0, 2), 16),
      g: parseInt(validColor.substr(2, 2), 16),
      b: parseInt(validColor.substr(4, 2), 16)
    };

    // Calculate Euclidean distance in RGB space
    const distance = Math.sqrt(
      Math.pow(validRGB.r - targetRGB.r, 2) +
      Math.pow(validRGB.g - targetRGB.g, 2) +
      Math.pow(validRGB.b - targetRGB.b, 2)
    );

    if (distance < minDistance) {
      minDistance = distance;
      closestColor = name as GmailLabelColor;
    }
  });

  return GMAIL_LABEL_COLORS[closestColor];
}

/**
 * Error messages for label operations
 */
export const LABEL_ERROR_MESSAGES = {
  INVALID_COLOR: 'Invalid label color. Please use one of the predefined Gmail label colors.',
  INVALID_COLOR_COMBINATION: 'Invalid text and background color combination.',
  COLOR_SUGGESTION: (original: string, suggested: typeof GMAIL_LABEL_COLORS[GmailLabelColor]) => 
    `The color "${original}" is not supported. Consider using the suggested color: Background: ${suggested.backgroundColor}, Text: ${suggested.textColor}`
} as const;

```

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

```typescript
import { TokenManager } from '../../../modules/accounts/token.js';
import { GoogleOAuthClient } from '../../../modules/accounts/oauth.js';

jest.mock('../../../modules/accounts/oauth.js');
jest.mock('../../../utils/logger.js');

describe('TokenManager', () => {
  let tokenManager: TokenManager;
  let mockOAuthClient: jest.Mocked<GoogleOAuthClient>;

  beforeEach(() => {
    mockOAuthClient = new GoogleOAuthClient() as jest.Mocked<GoogleOAuthClient>;
    tokenManager = new TokenManager(mockOAuthClient);

    // Mock filesystem operations
    jest.spyOn(tokenManager as any, 'loadToken').mockImplementation(async () => null);
    jest.spyOn(tokenManager as any, 'saveToken').mockImplementation(async () => {});
  });

  describe('autoRenewToken', () => {
    it('should return valid status for non-expired token', async () => {
      const validToken = {
        access_token: 'valid_token',
        refresh_token: 'refresh_token',
        expiry_date: Date.now() + 3600000 // 1 hour from now
      };

      (tokenManager as any).loadToken.mockResolvedValue(validToken);

      const result = await tokenManager.autoRenewToken('[email protected]');
      expect(result.success).toBe(true);
      expect(result.status).toBe('VALID');
      expect(result.token).toBe(validToken);
    });

    it('should attempt refresh for expired token', async () => {
      const expiredToken = {
        access_token: 'expired_token',
        refresh_token: 'refresh_token',
        expiry_date: Date.now() - 3600000 // 1 hour ago
      };

      const newToken = {
        access_token: 'new_token',
        refresh_token: 'refresh_token',
        expiry_date: Date.now() + 3600000
      };

      (tokenManager as any).loadToken.mockResolvedValue(expiredToken);
      mockOAuthClient.refreshToken.mockResolvedValue(newToken);

      const result = await tokenManager.autoRenewToken('[email protected]');
      expect(result.success).toBe(true);
      expect(result.status).toBe('REFRESHED');
      expect(result.token).toBe(newToken);
    });

    it('should handle invalid refresh token', async () => {
      const expiredToken = {
        access_token: 'expired_token',
        refresh_token: 'invalid_refresh_token',
        expiry_date: Date.now() - 3600000
      };

      (tokenManager as any).loadToken.mockResolvedValue(expiredToken);
      mockOAuthClient.refreshToken.mockRejectedValue(new Error('invalid_grant'));

      const result = await tokenManager.autoRenewToken('[email protected]');
      expect(result.success).toBe(false);
      expect(result.status).toBe('REFRESH_FAILED');
      expect(result.canRetry).toBe(false);
    });

    it('should handle temporary refresh failures', async () => {
      const expiredToken = {
        access_token: 'expired_token',
        refresh_token: 'refresh_token',
        expiry_date: Date.now() - 3600000
      };

      (tokenManager as any).loadToken.mockResolvedValue(expiredToken);
      mockOAuthClient.refreshToken.mockRejectedValue(new Error('network_error'));

      const result = await tokenManager.autoRenewToken('[email protected]');
      expect(result.success).toBe(false);
      expect(result.status).toBe('REFRESH_FAILED');
      expect(result.canRetry).toBe(true);
    });

    it('should handle missing token', async () => {
      (tokenManager as any).loadToken.mockResolvedValue(null);

      const result = await tokenManager.autoRenewToken('[email protected]');
      expect(result.success).toBe(false);
      expect(result.status).toBe('NO_TOKEN');
    });
  });
});

```

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

```typescript
import { google } from 'googleapis';
import { 
  GmailAttachment,
  IncomingGmailAttachment,
  OutgoingGmailAttachment,
  GmailError 
} from '../types.js';
import { AttachmentIndexService } from '../../attachments/index-service.js';

export class GmailAttachmentService {
  private static instance: GmailAttachmentService;
  private indexService: AttachmentIndexService;
  private gmailClient?: ReturnType<typeof google.gmail>;

  private constructor() {
    this.indexService = AttachmentIndexService.getInstance();
  }

  /**
   * Add attachment metadata to the index
   */
  addAttachment(messageId: string, attachment: {
    id: string;
    name: string;
    mimeType: string;
    size: number;
  }): void {
    this.indexService.addAttachment(messageId, attachment);
  }

  public static getInstance(): GmailAttachmentService {
    if (!GmailAttachmentService.instance) {
      GmailAttachmentService.instance = new GmailAttachmentService();
    }
    return GmailAttachmentService.instance;
  }

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

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

  /**
   * Get attachment content from Gmail
   */
  async getAttachment(
    email: string,
    messageId: string,
    filename: string
  ): Promise<IncomingGmailAttachment> {
    try {
      // Get original metadata from index
      const metadata = this.indexService.getMetadata(messageId, filename);
      if (!metadata) {
        throw new GmailError(
          'Attachment not found',
          'ATTACHMENT_ERROR',
          'Attachment metadata not found - message may need to be refreshed'
        );
      }

      const client = this.ensureClient();
      const { data } = await client.users.messages.attachments.get({
        userId: 'me',
        messageId,
        id: metadata.originalId,
      });

      if (!data.data) {
        throw new Error('No attachment data received');
      }

      return {
        id: metadata.originalId,
        content: data.data,
        size: metadata.size,
        name: metadata.filename,
        mimeType: metadata.mimeType,
      };
    } catch (error) {
      throw new GmailError(
        'Failed to get attachment',
        'ATTACHMENT_ERROR',
        `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
      );
    }
  }

  /**
   * Validate attachment content and size
   */
  validateAttachment(attachment: OutgoingGmailAttachment): void {
    if (!attachment.content) {
      throw new GmailError(
        'Invalid attachment',
        'VALIDATION_ERROR',
        'Attachment content is required'
      );
    }

    // Gmail's attachment size limit is 25MB
    const MAX_SIZE = 25 * 1024 * 1024;
    if (attachment.size > MAX_SIZE) {
      throw new GmailError(
        'Invalid attachment',
        'VALIDATION_ERROR',
        `Attachment size ${attachment.size} exceeds maximum allowed size ${MAX_SIZE}`
      );
    }
  }

  /**
   * Prepare attachment for sending
   */
  prepareAttachment(attachment: OutgoingGmailAttachment): {
    filename: string;
    mimeType: string;
    content: string;
  } {
    this.validateAttachment(attachment);
    
    return {
      filename: attachment.name,
      mimeType: attachment.mimeType,
      content: attachment.content,
    };
  }
}

```

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

```typescript
import { AttachmentIndexService } from './index-service.js';

/**
 * Service for managing attachment cleanup with intelligent scheduling
 */
export class AttachmentCleanupService {
  private cleanupInterval: NodeJS.Timeout | null = null;
  private readonly baseIntervalMs = 300000; // 5 minutes
  private readonly maxIntervalMs = 3600000; // 1 hour
  private currentIntervalMs: number;
  private lastCleanupTime: number = 0;
  private lastIndexSize: number = 0;

  constructor(private indexService: AttachmentIndexService) {
    this.currentIntervalMs = this.baseIntervalMs;
  }

  /**
   * Start the cleanup service with adaptive scheduling
   */
  start(): void {
    if (this.cleanupInterval) {
      return; // Already running
    }

    // Initial state
    this.lastIndexSize = this.indexService.size;
    this.lastCleanupTime = Date.now();

    // Run initial cleanup immediately
    this.cleanup();

    // Schedule next cleanup
    this.scheduleNextCleanup();
  }

  /**
   * Schedule next cleanup based on system activity
   */
  private scheduleNextCleanup(): void {
    if (this.cleanupInterval) {
      clearTimeout(this.cleanupInterval);
    }

    // Calculate next interval based on activity
    const nextInterval = this.currentIntervalMs;
    
    this.cleanupInterval = setTimeout(() => {
      this.cleanup();
      this.scheduleNextCleanup();
    }, nextInterval);
  }

  /**
   * Get current cleanup interval (for testing)
   */
  getCurrentInterval(): number {
    return this.currentIntervalMs;
  }

  /**
   * Notify the cleanup service of system activity
   * Call this when attachments are added or accessed
   */
  notifyActivity(): void {
    const currentSize = this.indexService.size;
    const sizeIncreased = currentSize > this.lastIndexSize;
    
    // Decrease interval if we're seeing increased activity
    if (sizeIncreased) {
      this.currentIntervalMs = Math.max(
        this.baseIntervalMs,
        this.currentIntervalMs * 0.75
      );
      
      // Force immediate cleanup if we're near capacity
      if (currentSize >= this.indexService.maxEntries * 0.9) {
        this.cleanup();
        this.scheduleNextCleanup();
      }
    } else {
      // Gradually increase interval during low activity
      this.currentIntervalMs = Math.min(
        this.maxIntervalMs,
        this.currentIntervalMs * 1.25
      );
    }

    this.lastIndexSize = currentSize;
  }

  /**
   * Stop the cleanup service
   */
  stop(): void {
    if (this.cleanupInterval) {
      clearTimeout(this.cleanupInterval);
      this.cleanupInterval = null;
    }
  }

  /**
   * For testing purposes only - clear all internal state
   */
  _reset(): void {
    this.stop();
    this.currentIntervalMs = this.baseIntervalMs;
    this.lastCleanupTime = 0;
    this.lastIndexSize = 0;
  }

  /**
   * Run cleanup with performance monitoring
   */
  private cleanup(): void {
    try {
      const startTime = process.hrtime();
      
      // Only run if enough time has passed since last cleanup
      const timeSinceLastCleanup = Date.now() - this.lastCleanupTime;
      if (timeSinceLastCleanup < this.baseIntervalMs / 2) {
        return;
      }

      // Run cleanup
      this.indexService.cleanExpiredEntries();
      this.lastCleanupTime = Date.now();

      // Monitor performance
      const [seconds, nanoseconds] = process.hrtime(startTime);
      const milliseconds = seconds * 1000 + nanoseconds / 1000000;
      
      // Adjust interval based on cleanup duration
      if (milliseconds > 100) { // If cleanup takes >100ms
        this.currentIntervalMs = Math.min(
          this.maxIntervalMs,
          this.currentIntervalMs * 1.5
        );
      }
    } catch (error) {
      console.error('Error during attachment cleanup:', error);
    }
  }
}

```

--------------------------------------------------------------------------------
/scripts/build-local.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash
set -e

# Force immediate output
exec 1>&1

# This script provides a lightweight local development build pipeline
# that mirrors our CI approach but optimized for speed and local iteration.
# It performs all build steps from linting through Docker image creation,
# providing minimal but clear status output.

# Parse command line arguments
VERBOSE=false
IMAGE_TAG="google-workspace-mcp:local"
while [[ "$#" -gt 0 ]]; do
    case $1 in
        --verbose) VERBOSE=true ;;
        --tag) IMAGE_TAG="$2"; shift ;;
        *) echo "Unknown parameter: $1"; exit 1 ;;
    esac
    shift
done

# Create temp directory for logs if it doesn't exist
TEMP_DIR="/tmp/google-workspace-mcp"
mkdir -p "$TEMP_DIR"

# Setup colored output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
CHECK_MARK="✓"
X_MARK="✗"
WARNING_MARK="⚠"

# Function to check log file size and show warning if needed
check_log_size() {
    local log_file=$1
    if [ -f "$log_file" ]; then
        local line_count=$(wc -l < "$log_file")
        if [ $line_count -gt 100 ]; then
            echo -e "${YELLOW}${WARNING_MARK} Large log file detected ($line_count lines)${NC}"
            echo "  Tips for viewing large logs:"
            echo "  • head -n 20 $log_file     (view first 20 lines)"
            echo "  • tail -n 20 $log_file     (view last 20 lines)"
            echo "  • less $log_file           (scroll through file)"
            echo "  • grep 'error' $log_file   (search for specific terms)"
            echo "  • use pageless mode with tools when viewing files"
        fi
    fi
}

# Function to run a step and show its status
run_step() {
    local step_name=$1
    local log_file="$TEMP_DIR/$2.log"
    local command=$3

    echo -n "→ $step_name... "

    if [ "$VERBOSE" = true ]; then
        if eval "$command"; then
            echo -e "${GREEN}${CHECK_MARK} Success${NC}"
            return 0
        else
            echo -e "${RED}${X_MARK} Failed${NC}"
            return 1
        fi
    else
        if eval "$command > '$log_file' 2>&1"; then
            echo -e "${GREEN}${CHECK_MARK} Success${NC} (log: $log_file)"
            check_log_size "$log_file"
            return 0
        else
            echo -e "${RED}${X_MARK} Failed${NC} (see details in $log_file)"
            check_log_size "$log_file"
            return 1
        fi
    fi
}

# Install dependencies
run_step "Installing dependencies" "npm-install" "npm install" || exit 1

# Type check
run_step "Type checking" "type-check" "npm run type-check" || exit 1

# Run linting
run_step "Linting" "lint" "npm run lint" || exit 1

# Run tests (commented out since they're a placeholder)
# run_step "Testing" "test" "npm run test || true"

# Build TypeScript
run_step "Building TypeScript" "build" "npm run build" || exit 1

# Build Docker image using the local Dockerfile
echo "→ Building Docker image..."
if [ "$VERBOSE" = true ]; then
    if docker build -t "$IMAGE_TAG" -f "Dockerfile.local" .; then
        echo -e "${GREEN}${CHECK_MARK} Docker build successful${NC}"
    else
        echo -e "${RED}${X_MARK} Docker build failed${NC}"
        exit 1
    fi
else
    DOCKER_LOG="$TEMP_DIR/docker-build.log"
    if docker build -t "$IMAGE_TAG" -f "Dockerfile.local" . > "$DOCKER_LOG" 2>&1; then
        echo -e "${GREEN}${CHECK_MARK} Docker build successful${NC} (log: $DOCKER_LOG)"
        check_log_size "$DOCKER_LOG"
    else
        echo -e "${RED}${X_MARK} Docker build failed${NC} (see details in $DOCKER_LOG)"
        check_log_size "$DOCKER_LOG"
        exit 1
    fi
fi

echo -e "\n${GREEN}Build complete!${NC} Image tagged as $IMAGE_TAG"
echo "To change the image tag, use: ./scripts/build-local.sh --tag <your-tag>"
echo "This build uses Dockerfile.local which is optimized for local development."

```

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

```typescript
import { google } from 'googleapis';
import {
  GetGmailSettingsParams,
  GetGmailSettingsResponse,
  GmailError
} from '../types.js';

export class SettingsService {
  constructor(
    private gmailClient?: ReturnType<typeof google.gmail>
  ) {}

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

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

  async getWorkspaceGmailSettings({ email }: GetGmailSettingsParams): Promise<GetGmailSettingsResponse> {
    try {
      // Get profile data
      const client = this.ensureClient();
      const { data: profile } = await client.users.getProfile({
        userId: 'me'
      });

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

      const response: GetGmailSettingsResponse = {
        profile: {
          emailAddress: profile.emailAddress ?? '',
          messagesTotal: typeof profile.messagesTotal === 'number' ? profile.messagesTotal : 0,
          threadsTotal: typeof profile.threadsTotal === 'number' ? profile.threadsTotal : 0,
          historyId: profile.historyId ?? ''
        },
        settings: {
          ...(language?.displayLanguage && {
            language: {
              displayLanguage: language.displayLanguage
            }
          }),
          ...(autoForwarding && {
            autoForwarding: {
              enabled: Boolean(autoForwarding.enabled),
              ...(autoForwarding.emailAddress && {
                emailAddress: autoForwarding.emailAddress
              })
            }
          }),
          ...(imap && {
            imap: {
              enabled: Boolean(imap.enabled),
              ...(typeof imap.autoExpunge === 'boolean' && {
                autoExpunge: imap.autoExpunge
              }),
              ...(imap.expungeBehavior && {
                expungeBehavior: imap.expungeBehavior
              })
            }
          }),
          ...(pop && {
            pop: {
              enabled: Boolean(pop.accessWindow),
              ...(pop.accessWindow && {
                accessWindow: pop.accessWindow
              })
            }
          }),
          ...(vacation && {
            vacationResponder: {
              enabled: Boolean(vacation.enableAutoReply),
              ...(vacation.startTime && {
                startTime: vacation.startTime
              }),
              ...(vacation.endTime && {
                endTime: vacation.endTime
              }),
              ...(vacation.responseSubject && {
                responseSubject: vacation.responseSubject
              }),
              ...((vacation.responseBodyHtml || vacation.responseBodyPlainText) && {
                message: vacation.responseBodyHtml ?? vacation.responseBodyPlainText ?? ''
              })
            }
          })
        }
      };

      return response;
    } catch (error) {
      if (error instanceof GmailError) {
        throw error;
      }
      throw new GmailError(
        'Failed to get Gmail settings',
        'SETTINGS_ERROR',
        `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
      );
    }
  }
}

```

--------------------------------------------------------------------------------
/src/services/base/BaseGoogleService.ts:
--------------------------------------------------------------------------------

```typescript
import { OAuth2Client } from 'google-auth-library';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { getAccountManager } from '../../modules/accounts/index.js';

/**
 * Base error class for Google services
 */
export class GoogleServiceError extends McpError {
  constructor(
    message: string,
    code: string,
    details?: string
  ) {
    super(ErrorCode.InternalError, message, { code, details });
  }
}

/**
 * Configuration interface for Google services
 */
export interface GoogleServiceConfig {
  serviceName: string;
  version: string;
}

/**
 * Base class for Google service implementations.
 * Provides common functionality for authentication, error handling, and client management.
 */
export abstract class BaseGoogleService<TClient> {
  protected oauth2Client?: OAuth2Client;
  private apiClients: Map<string, TClient> = new Map();
  private readonly serviceName: string;

  constructor(config: GoogleServiceConfig) {
    this.serviceName = config.serviceName;
  }

  /**
   * Initializes the service by setting up OAuth2 client
   */
  protected async initialize(): Promise<void> {
    try {
      const accountManager = getAccountManager();
      this.oauth2Client = await accountManager.getAuthClient();
    } catch (error) {
      throw this.handleError(error, 'Failed to initialize service');
    }
  }

  /**
   * Gets an authenticated API client for the service
   * 
   * @param email - The email address to get a client for
   * @param clientFactory - Function to create the specific Google API client
   * @returns The authenticated API client
   */
  protected async getAuthenticatedClient(
    email: string,
    clientFactory: (auth: OAuth2Client) => TClient
  ): Promise<TClient> {
    if (!this.oauth2Client) {
      throw new GoogleServiceError(
        `${this.serviceName} client not initialized`,
        'CLIENT_ERROR',
        'Please ensure the service is initialized'
      );
    }

    const existingClient = this.apiClients.get(email);
    if (existingClient) {
      return existingClient;
    }

    try {
      const accountManager = getAccountManager();
      const tokenStatus = await accountManager.validateToken(email);

      if (!tokenStatus.valid || !tokenStatus.token) {
        throw new GoogleServiceError(
          `${this.serviceName} authentication required`,
          'AUTH_REQUIRED',
          'Please authenticate the account'
        );
      }

      this.oauth2Client.setCredentials(tokenStatus.token);
      const client = clientFactory(this.oauth2Client);
      this.apiClients.set(email, client);
      return client;
    } catch (error) {
      throw this.handleError(error, 'Failed to get authenticated client');
    }
  }

  /**
   * Common error handler for Google service operations
   */
  protected handleError(error: unknown, context: string): GoogleServiceError {
    if (error instanceof GoogleServiceError) {
      return error;
    }

    return new GoogleServiceError(
      context,
      'SERVICE_ERROR',
      `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
    );
  }

  /**
   * Validates required scopes are present for an operation
   */
  protected async validateScopes(email: string, requiredScopes: string[]): Promise<void> {
    try {
      const accountManager = getAccountManager();
      const tokenInfo = await accountManager.validateToken(email);

      if (!tokenInfo.requiredScopes) {
        throw new GoogleServiceError(
          'No scopes found in token',
          'SCOPE_ERROR',
          'Token does not contain scope information'
        );
      }

      const missingScopes = requiredScopes.filter(
        scope => !tokenInfo.requiredScopes?.includes(scope)
      );

      if (missingScopes.length > 0) {
        throw new GoogleServiceError(
          'Insufficient permissions',
          'SCOPE_ERROR',
          `Missing required scopes: ${missingScopes.join(', ')}`
        );
      }
    } catch (error) {
      throw this.handleError(error, 'Failed to validate scopes');
    }
  }
}

```

--------------------------------------------------------------------------------
/docs/TOOL_DISCOVERY.md:
--------------------------------------------------------------------------------

```markdown
# Tool Discovery and Aliases

The Google Workspace MCP server provides several features to make tools more discoverable and easier to use:

## Tool Categories

Tools are organized into logical categories with clear dependencies:

### Account Management (Required First)
- Authentication and account management
  - list_workspace_accounts (foundation for all operations)
  - authenticate_workspace_account
  - remove_workspace_account

### Gmail Management
- Messages
  - search_workspace_emails
  - send_workspace_email
  - get_workspace_gmail_settings
  - manage_workspace_draft
- Labels
  - manage_workspace_label
  - manage_workspace_label_assignment
  - manage_workspace_label_filter

### Calendar Management
- Events
  - list_workspace_calendar_events
  - get_workspace_calendar_event
  - manage_workspace_calendar_event
  - create_workspace_calendar_event
  - delete_workspace_calendar_event

### Drive Management
- Files
  - list_drive_files
  - search_drive_files
  - upload_drive_file
  - download_drive_file
- Folders
  - create_drive_folder
- Permissions
  - update_drive_permissions
- Operations
  - delete_drive_file

IMPORTANT: The list_workspace_accounts tool MUST be called 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

## Tool Aliases

Most tools support multiple aliases for more intuitive usage. For example:

```javascript
// All of these are equivalent:
create_workspace_label
create_label
new_label
create_gmail_label
```

## Improved Error Messages

When a tool name is not found, the server provides helpful suggestions:

```
Tool 'create_gmail_lable' not found.

Did you mean:
- create_workspace_label (Gmail/Labels)
  Aliases: create_label, new_label, create_gmail_label

Available categories:
- Gmail/Labels: create_label, update_label, delete_label
- Gmail/Messages: send_email, search_emails
- Calendar/Events: create_event, update_event, delete_event
```

## Tool Metadata

Each tool includes:

- Category: Logical grouping for organization
- Aliases: Alternative names for the tool
- Description: Detailed usage information
- Input Schema: Required and optional parameters

## Best Practices

1. Use the most specific tool name when possible
2. Check error messages for similar tool suggestions
3. Use the list_workspace_tools command to see all available tools
4. Refer to tool categories for related functionality

## Examples

### Creating a Label

```javascript
// Any of these work:
create_workspace_label({
  email: "[email protected]",
  name: "Important/Projects",
  messageListVisibility: "show",
  labelListVisibility: "labelShow",
  color: {
    textColor: "#000000",
    backgroundColor: "#E7E7E7"
  }
})

create_label({
  email: "[email protected]",
  name: "Important/Projects"
})
```

### Creating a Label Filter

```javascript
// Create a filter to automatically label incoming emails
create_workspace_label_filter({
  email: "[email protected]",
  labelId: "Label_123",
  criteria: {
    from: ["[email protected]"],
    subject: "Project Update",
    hasAttachment: true
  },
  actions: {
    addLabel: true,
    markImportant: true
  }
})
```

### Managing Message Labels

```javascript
// Add/remove labels from a message
modify_workspace_message_labels({
  email: "[email protected]",
  messageId: "msg_123",
  addLabelIds: ["Label_123"],
  removeLabelIds: ["UNREAD"]
})
```

### Sending an Email

```javascript
// These are equivalent:
send_workspace_email({
  email: "[email protected]",
  to: ["[email protected]"],
  subject: "Hello",
  body: "Message content",
  cc: ["[email protected]"]
})

send_email({
  email: "[email protected]",
  to: ["[email protected]"],
  subject: "Hello",
  body: "Message content"
})
```

## Future Improvements

- Category descriptions and documentation
- Tool relationship mapping
- Common usage patterns and workflows
- Interactive tool discovery
- Workflow templates for common tasks
- Error handling best practices
- Performance optimization guidelines
- Security and permission management

```

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

```typescript
import { google } from "googleapis";
import {
  BaseGoogleService,
  GoogleServiceError
} from "../base/BaseGoogleService.js";
import { McpError } from "@modelcontextprotocol/sdk/types.js";
import {
  GetContactsParams,
  GetContactsResponse,
  ContactsError
} from "../../modules/contacts/types.js";
import { CONTACTS_SCOPES } from "../../modules/contacts/scopes.js";

// Type alias for the Google People API client
type PeopleApiClient = ReturnType<typeof google.people>;

/**
 * Contacts service implementation extending BaseGoogleService.
 * Handles Google Contacts (People API) specific operations.
 */
export class ContactsService extends BaseGoogleService<PeopleApiClient> {
  constructor() {
    super({
      serviceName: "people", // Use 'people' for the People API
      version: "v1"
    });
    // Initialize immediately or ensure initialized before first use
    this.initialize();
  }

  /**
   * Gets an authenticated People API client for the specified account.
   */
  private async getPeopleClient(email: string): Promise<PeopleApiClient> {
    // The clientFactory function tells BaseGoogleService how to create the specific client
    return this.getAuthenticatedClient(email, (auth) =>
      google.people({ version: "v1", auth })
    );
  }

  /**
   * Retrieves contacts for the specified user account.
   */
  async getContacts(params: GetContactsParams): Promise<GetContactsResponse> {
    const { email, pageSize, pageToken, personFields } = params;

    if (!personFields) {
      throw new ContactsError(
        "Missing required parameter: personFields",
        "INVALID_PARAMS",
        'Specify the fields to retrieve (e.g. "names,emailAddresses")'
      );
    }

    try {
      // Ensure necessary scopes are granted
      await this.validateScopes(email, [CONTACTS_SCOPES.READONLY]);

      const peopleApi = await this.getPeopleClient(email);

      const response = await peopleApi.people.connections.list({
        resourceName: "people/me", // 'people/me' refers to the authenticated user
        pageSize: pageSize,
        pageToken: pageToken,
        personFields: personFields,
        // requestSyncToken: true // Consider adding for sync capabilities later
      });

      // We might want to add more robust mapping/validation here
      // For now we assume the response structure matches GetContactsResponse
      // Note: googleapis types might use 'null' where we defined optional fields ('undefined')
      // Need to handle potential nulls if strict null checks are enabled
      return response.data as GetContactsResponse;
    } catch (error) {
      // Handle known GoogleServiceError specifically
      if (error instanceof GoogleServiceError) {
        // Assuming GoogleServiceError inherits message and data from McpError
        // Use type assertion as the linter seems unsure
        const gError = error as McpError & {
          data?: { code?: string; details?: string };
        };
        throw new ContactsError(
          gError.message || "Error retrieving contacts", // Fallback message
          gError.data?.code || "GOOGLE_SERVICE_ERROR", // Code from data
          gError.data?.details // Details from data
        );
      }
      // Handle other potential errors (e.g. network errors)
      else if (error instanceof Error) {
        throw new ContactsError(
          `Failed to retrieve contacts: ${error.message}`,
          "UNKNOWN_API_ERROR" // More specific code
        );
      }
      // Handle non-Error throws
      else {
        throw new ContactsError(
          "Failed to retrieve contacts due to an unknown issue",
          "UNKNOWN_INTERNAL_ERROR" // More specific code
        );
      }
    }
  }

  // Add other methods like searchContacts, createContact, updateContact, deleteContact later
}

// Optional: Export a singleton instance if needed by the module structure
// let contactsServiceInstance: ContactsService | null = null;
// export function getContactsService(): ContactsService {
//   if (!contactsServiceInstance) {
//     contactsServiceInstance = new ContactsService();
//   }
//   return contactsServiceInstance;
// }

```

--------------------------------------------------------------------------------
/src/api/request.ts:
--------------------------------------------------------------------------------

```typescript
import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
import { ApiRequestParams, GoogleApiError } from '../types.js';

interface ServiceVersionMap {
  [key: string]: string;
}

export class GoogleApiRequest {
  private readonly serviceVersions: ServiceVersionMap = {
    gmail: 'v1',
    drive: 'v3',
    sheets: 'v4',
    calendar: 'v3'
  };

  constructor(private authClient: OAuth2Client) {}

  async makeRequest({
    endpoint,
    method,
    params = {},
    token
  }: ApiRequestParams): Promise<any> {
    try {
      // Set up authentication
      this.authClient.setCredentials({
        access_token: token
      });

      // Parse the endpoint to get service and method
      const [service, ...methodParts] = endpoint.split('.');
      const methodName = methodParts.join('.');

      // Get the Google API service
      const googleService = await this.getGoogleService(service);

      // Navigate to the method in the service
      const apiMethod = this.getApiMethod(googleService, methodName);

      // Make the API request with proper context binding
      const response = await apiMethod.call(googleService, {
        ...params,
        auth: this.authClient
      });

      return response.data;
    } catch (error: any) {
      throw this.handleApiError(error);
    }
  }

  private async getGoogleService(service: string): Promise<any> {
    const version = this.serviceVersions[service];
    if (!version) {
      throw new GoogleApiError(
        `Service "${service}" is not supported`,
        'SERVICE_NOT_SUPPORTED',
        `Supported services are: ${Object.keys(this.serviceVersions).join(', ')}`
      );
    }

    const serviceConstructor = (google as any)[service];
    if (!serviceConstructor) {
      throw new GoogleApiError(
        `Failed to initialize ${service} service`,
        'SERVICE_INIT_FAILED',
        'Please check the service name and try again'
      );
    }

    return serviceConstructor({ version, auth: this.authClient });
  }

  private getApiMethod(service: any, methodPath: string): (params: Record<string, any>) => Promise<any> {
    const method = methodPath.split('.').reduce((obj: any, part) => obj?.[part], service);

    if (typeof method !== 'function') {
      throw new GoogleApiError(
        `Method "${methodPath}" not found`,
        'METHOD_NOT_FOUND',
        'Please check the method name and try again'
      );
    }

    return method;
  }

  private handleApiError(error: any): never {
    if (error instanceof GoogleApiError) {
      throw error;
    }

    // Handle Google API specific errors
    if (error.response) {
      const { status, data } = error.response;
      const errorInfo = this.getErrorInfo(status, data);
      throw new GoogleApiError(
        errorInfo.message,
        errorInfo.code,
        errorInfo.resolution
      );
    }

    // Handle network or other errors
    throw new GoogleApiError(
      error.message || 'Unknown error occurred',
      'API_REQUEST_ERROR',
      'Check your network connection and try again'
    );
  }

  private getErrorInfo(status: number, data: any): { message: string; code: string; resolution: string } {
    const errorMap: Record<number, { code: string; resolution: string }> = {
      400: {
        code: 'BAD_REQUEST',
        resolution: 'Check the request parameters and try again'
      },
      401: {
        code: 'UNAUTHORIZED',
        resolution: 'Token may be expired. Try refreshing the token'
      },
      403: {
        code: 'FORBIDDEN',
        resolution: 'Insufficient permissions. Check required scopes'
      },
      404: {
        code: 'NOT_FOUND',
        resolution: 'Resource not found. Verify the endpoint and parameters'
      },
      429: {
        code: 'RATE_LIMIT',
        resolution: 'Rate limit exceeded. Try again later'
      },
      500: {
        code: 'SERVER_ERROR',
        resolution: 'Internal server error. Please try again later'
      },
      503: {
        code: 'SERVICE_UNAVAILABLE',
        resolution: 'Service is temporarily unavailable. Please try again later'
      }
    };

    const errorInfo = errorMap[status] || {
      code: `API_ERROR_${status}`,
      resolution: 'Check the API documentation for more details'
    };

    return {
      message: data.error?.message || 'API request failed',
      code: errorInfo.code,
      resolution: errorInfo.resolution
    };
  }
}

```

--------------------------------------------------------------------------------
/docs/EXAMPLES.md:
--------------------------------------------------------------------------------

```markdown
# Google Workspace MCP Examples

This document provides examples of using the Google Workspace MCP tools.

## Account Management

```typescript
// List configured accounts
const accounts = await mcp.callTool('list_workspace_accounts', {});

// Authenticate a new account
const auth = await mcp.callTool('authenticate_workspace_account', {
  email: '[email protected]'
});

// Remove an account
await mcp.callTool('remove_workspace_account', {
  email: '[email protected]'
});
```

## Gmail Operations

### Messages

```typescript
// Search emails
const emails = await mcp.callTool('search_workspace_emails', {
  email: '[email protected]',
  search: {
    from: '[email protected]',
    subject: 'Important Meeting',
    after: '2024-01-01',
    hasAttachment: true
  }
});

// Example response with simplified attachment format (v1.1)
{
  "emails": [{
    "id": "msg123",
    "subject": "Important Meeting",
    "from": "[email protected]",
    "hasAttachment": true,
    "attachments": [{
      "name": "presentation.pdf"
    }]
  }]
}

// Send email
await mcp.callTool('send_workspace_email', {
  email: '[email protected]',
  to: ['[email protected]'],
  subject: 'Hello',
  body: 'Message content'
});
```

### Labels

```typescript
// Create label
await mcp.callTool('manage_workspace_label', {
  email: '[email protected]',
  action: 'create',
  data: {
    name: 'Projects/Active',
    labelListVisibility: 'labelShow',
    messageListVisibility: 'show'
  }
});

// Apply label to message
await mcp.callTool('manage_workspace_label_assignment', {
  email: '[email protected]',
  action: 'add',
  messageId: 'msg123',
  labelIds: ['label123']
});
```

### Drafts

```typescript
// Create draft
const draft = await mcp.callTool('manage_workspace_draft', {
  email: '[email protected]',
  action: 'create',
  data: {
    to: ['[email protected]'],
    subject: 'Draft Message',
    body: 'Draft content'
  }
});

// Send draft
await mcp.callTool('manage_workspace_draft', {
  email: '[email protected]',
  action: 'send',
  draftId: draft.id
});
```

## Calendar Operations

### Events

```typescript
// List calendar events
const events = await mcp.callTool('list_workspace_calendar_events', {
  email: '[email protected]',
  timeMin: '2024-02-01T00:00:00Z',
  timeMax: '2024-02-28T23:59:59Z'
});

// Create event
await mcp.callTool('create_workspace_calendar_event', {
  email: '[email protected]',
  summary: 'Team Meeting',
  start: {
    dateTime: '2024-02-20T10:00:00-06:00',
    timeZone: 'America/Chicago'
  },
  end: {
    dateTime: '2024-02-20T11:00:00-06:00',
    timeZone: 'America/Chicago'
  },
  attendees: [
    { email: '[email protected]' }
  ]
});

// Respond to event
await mcp.callTool('manage_workspace_calendar_event', {
  email: '[email protected]',
  eventId: 'evt123',
  action: 'accept',
  comment: 'Looking forward to it!'
});
```

## Drive Operations

### File Management

```typescript
// List files
const files = await mcp.callTool('list_drive_files', {
  email: '[email protected]',
  options: {
    folderId: 'folder123',
    pageSize: 100
  }
});

// Search files
const searchResults = await mcp.callTool('search_drive_files', {
  email: '[email protected]',
  options: {
    fullText: 'project proposal',
    mimeType: 'application/pdf'
  }
});

// Upload file
const uploadedFile = await mcp.callTool('upload_drive_file', {
  email: '[email protected]',
  options: {
    name: 'document.pdf',
    content: 'base64_encoded_content',
    mimeType: 'application/pdf',
    parents: ['folder123']
  }
});

// Download file
const fileContent = await mcp.callTool('download_drive_file', {
  email: '[email protected]',
  fileId: 'file123',
  mimeType: 'application/pdf'  // For Google Workspace files
});

// Delete file
await mcp.callTool('delete_drive_file', {
  email: '[email protected]',
  fileId: 'file123'
});
```

### Folder Operations

```typescript
// Create folder
const folder = await mcp.callTool('create_drive_folder', {
  email: '[email protected]',
  name: 'Project Documents',
  parentId: 'parent123'  // Optional
});
```

### Permissions

```typescript
// Update file permissions
await mcp.callTool('update_drive_permissions', {
  email: '[email protected]',
  options: {
    fileId: 'file123',
    role: 'writer',
    type: 'user',
    emailAddress: '[email protected]'
  }
});

// Share with domain
await mcp.callTool('update_drive_permissions', {
  email: '[email protected]',
  options: {
    fileId: 'file123',
    role: 'reader',
    type: 'domain',
    domain: 'example.com'
  }
});

```

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

```typescript
import { ToolRegistry, ToolMetadata } from '../registry.js';

describe('ToolRegistry', () => {
  const mockTools: ToolMetadata[] = [
    {
      name: 'create_workspace_label',
      category: 'Gmail/Labels',
      description: 'Create a new label',
      aliases: ['create_label', 'new_label', 'create_gmail_label'],
      inputSchema: {
        type: 'object',
        properties: {
          name: { type: 'string' }
        }
      }
    },
    {
      name: 'send_workspace_email',
      category: 'Gmail/Messages',
      description: 'Send an email',
      aliases: ['send_email', 'send_mail'],
      inputSchema: {
        type: 'object',
        properties: {
          to: { type: 'string' }
        }
      }
    },
    {
      name: 'create_workspace_calendar_event',
      category: 'Calendar/Events',
      description: 'Create calendar event',
      aliases: ['create_event', 'schedule_event'],
      inputSchema: {
        type: 'object',
        properties: {
          title: { type: 'string' }
        }
      }
    }
  ];

  let registry: ToolRegistry;

  beforeEach(() => {
    registry = new ToolRegistry(mockTools);
  });

  describe('getTool', () => {
    it('should find tool by main name', () => {
      const tool = registry.getTool('create_workspace_label');
      expect(tool).toBeDefined();
      expect(tool?.name).toBe('create_workspace_label');
    });

    it('should find tool by alias', () => {
      const tool = registry.getTool('create_gmail_label');
      expect(tool).toBeDefined();
      expect(tool?.name).toBe('create_workspace_label');
    });

    it('should return undefined for unknown tool', () => {
      const tool = registry.getTool('nonexistent_tool');
      expect(tool).toBeUndefined();
    });
  });

  describe('getAllTools', () => {
    it('should return all registered tools', () => {
      const tools = registry.getAllTools();
      expect(tools).toHaveLength(3);
      expect(tools.map(t => t.name)).toContain('create_workspace_label');
      expect(tools.map(t => t.name)).toContain('send_workspace_email');
      expect(tools.map(t => t.name)).toContain('create_workspace_calendar_event');
    });
  });

  describe('getCategories', () => {
    it('should return tools organized by category', () => {
      const categories = registry.getCategories();
      expect(categories).toHaveLength(3);
      
      const categoryNames = categories.map(c => c.name);
      expect(categoryNames).toContain('Gmail/Labels');
      expect(categoryNames).toContain('Gmail/Messages');
      expect(categoryNames).toContain('Calendar/Events');

      const labelCategory = categories.find(c => c.name === 'Gmail/Labels');
      expect(labelCategory?.tools).toHaveLength(1);
      expect(labelCategory?.tools[0].name).toBe('create_workspace_label');
    });
  });

  describe('findSimilarTools', () => {
    it('should find similar tools by name', () => {
      const similar = registry.findSimilarTools('create_label_workspace');
      expect(similar).toHaveLength(1);
      expect(similar[0].name).toBe('create_workspace_label');
    });

    it('should find similar tools by alias', () => {
      const similar = registry.findSimilarTools('gmail_label_create');
      expect(similar).toHaveLength(1);
      expect(similar[0].name).toBe('create_workspace_label');
    });

    it('should respect maxSuggestions limit', () => {
      const similar = registry.findSimilarTools('create', 2);
      expect(similar).toHaveLength(2);
    });
  });

  describe('formatErrorWithSuggestions', () => {
    it('should format error message with suggestions', () => {
      const message = registry.formatErrorWithSuggestions('create_gmail_lable');
      expect(message).toContain('Tool \'create_gmail_lable\' not found');
      expect(message).toContain('Did you mean:');
      expect(message).toContain('create_workspace_label (Gmail/Labels)');
      expect(message).toContain('Available categories:');
    });

    it('should include aliases in error message', () => {
      const message = registry.formatErrorWithSuggestions('send_mail_workspace');
      expect(message).toContain('send_workspace_email');
      expect(message).toContain('Aliases: send_email, send_mail');
    });
  });

  describe('getAllToolNames', () => {
    it('should return all tool names including aliases', () => {
      const names = registry.getAllToolNames();
      expect(names).toContain('create_workspace_label');
      expect(names).toContain('create_gmail_label');
      expect(names).toContain('send_workspace_email');
      expect(names).toContain('send_mail');
    });
  });
});

```

--------------------------------------------------------------------------------
/src/utils/token.ts:
--------------------------------------------------------------------------------

```typescript
import fs from 'fs/promises';
import path from 'path';
import { TokenData, GoogleApiError, Account } from '../types.js';

const ENV_PREFIX = 'GOOGLE_TOKEN_';

export class TokenManager {
  private readonly credentialsDir: string;
  private readonly envTokens: Map<string, TokenData>;

  constructor() {
    this.credentialsDir = process.env.CREDENTIALS_DIR || path.resolve('config', 'credentials');
    this.envTokens = new Map();
    this.loadEnvTokens();
  }

  private loadEnvTokens(): void {
    for (const [key, value] of Object.entries(process.env)) {
      if (key.startsWith(ENV_PREFIX) && value) {
        try {
          const email = key
            .slice(ENV_PREFIX.length)
            .toLowerCase()
            .replace(/_/g, '.');
          const tokenData = JSON.parse(
            Buffer.from(value, 'base64').toString()
          ) as TokenData;
          this.envTokens.set(email, tokenData);
        } catch (error) {
          console.warn(`Failed to parse token from env var ${key}:`, error);
        }
      }
    }
  }

  private getTokenPath(email: string): string {
    const sanitizedEmail = email.replace(/[@.]/g, '-');
    return path.join(this.credentialsDir, `${sanitizedEmail}.token.json`);
  }

  private updateEnvToken(email: string, tokenData: TokenData): void {
    const safeEmail = email.replace(/[@.]/g, '_').toUpperCase();
    process.env[`${ENV_PREFIX}${safeEmail}`] = Buffer.from(
      JSON.stringify(tokenData)
    ).toString('base64');
    this.envTokens.set(email, tokenData);
  }

  async saveToken(email: string, tokenData: TokenData): Promise<void> {
    try {
      // Save to file system
      const tokenPath = this.getTokenPath(email);
      await fs.writeFile(tokenPath, JSON.stringify(tokenData, null, 2));
      
      // Update environment variable
      this.updateEnvToken(email, tokenData);
    } catch (error) {
      throw new GoogleApiError(
        'Failed to save token',
        'TOKEN_SAVE_ERROR',
        'Please ensure the directory specified by CREDENTIALS_DIR is writable'
      );
    }
  }

  async loadToken(email: string): Promise<TokenData | null> {
    // First try to load from environment variables
    const envToken = this.envTokens.get(email);
    if (envToken) {
      return envToken;
    }

    // Fall back to file system
    try {
      const tokenPath = this.getTokenPath(email);
      const data = await fs.readFile(tokenPath, 'utf-8');
      const token = JSON.parse(data) as TokenData;
      
      // Update environment variable for future use
      this.updateEnvToken(email, token);
      
      return token;
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
        return null;
      }
      throw new GoogleApiError(
        'Failed to load token',
        'TOKEN_LOAD_ERROR',
        'Token file may be corrupted or inaccessible'
      );
    }
  }

  async deleteToken(email: string): Promise<void> {
    try {
      // Remove from file system
      const tokenPath = this.getTokenPath(email);
      await fs.unlink(tokenPath);
      
      // Remove from environment
      const safeEmail = email.replace(/[@.]/g, '_').toUpperCase();
      delete process.env[`${ENV_PREFIX}${safeEmail}`];
      this.envTokens.delete(email);
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
        throw new GoogleApiError(
          'Failed to delete token',
          'TOKEN_DELETE_ERROR',
          'Please ensure you have permission to delete the token file in CREDENTIALS_DIR'
        );
      }
    }
  }

  isTokenExpired(tokenData: TokenData): boolean {
    // Consider token expired 5 minutes before actual expiry
    const expiryBuffer = 5 * 60 * 1000; // 5 minutes in milliseconds
    return Date.now() >= tokenData.expiry_date - expiryBuffer;
  }

  hasRequiredScopes(tokenData: TokenData, requiredScopes: string[]): boolean {
    const tokenScopes = new Set(tokenData.scope.split(' '));
    return requiredScopes.every(scope => tokenScopes.has(scope));
  }

  async getTokenStatus(email: string): Promise<Account['auth_status']> {
    const token = await this.loadToken(email);
    if (!token) {
      return { has_token: false };
    }

    return {
      has_token: true,
      scopes: token.scope.split(' '),
      expires: token.expiry_date
    };
  }

  async validateToken(email: string, requiredScopes: string[]): Promise<{
    valid: boolean;
    token?: TokenData;
    reason?: string;
  }> {
    const token = await this.loadToken(email);
    
    if (!token) {
      return { valid: false, reason: 'Token not found' };
    }

    if (this.isTokenExpired(token)) {
      return { valid: false, token, reason: 'Token expired' };
    }

    if (!this.hasRequiredScopes(token, requiredScopes)) {
      return { valid: false, reason: 'Insufficient scopes' };
    }

    return { valid: true, token };
  }
}

```

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

```typescript
import { google } from 'googleapis';
import { BaseGoogleService, GoogleServiceError } from '../base/BaseGoogleService.js';
import {
  GetEventsParams,
  CreateEventParams,
  EventResponse,
  CreateEventResponse,
  CalendarModuleConfig
} from '../../modules/calendar/types.js';

/**
 * Google Calendar Service Implementation extending BaseGoogleService.
 * Provides calendar functionality while leveraging common Google API patterns.
 */
export class CalendarService extends BaseGoogleService<ReturnType<typeof google.calendar>> {
  constructor(config?: CalendarModuleConfig) {
    super({
      serviceName: 'calendar',
      version: 'v3'
    });
  }

  /**
   * Get an authenticated Calendar client
   */
  private async getCalendarClient(email: string) {
    return this.getAuthenticatedClient(
      email,
      (auth) => google.calendar({ version: 'v3', auth })
    );
  }

  /**
   * Retrieve calendar events with optional filtering
   */
  async getEvents({ email, query, maxResults = 10, timeMin, timeMax }: GetEventsParams): Promise<EventResponse[]> {
    try {
      const calendar = await this.getCalendarClient(email);

      // Prepare search parameters
      const params: any = {
        calendarId: 'primary',
        maxResults,
        singleEvents: true,
        orderBy: 'startTime'
      };

      if (query) {
        params.q = query;
      }

      if (timeMin) {
        params.timeMin = new Date(timeMin).toISOString();
      }

      if (timeMax) {
        params.timeMax = new Date(timeMax).toISOString();
      }

      // List events matching criteria
      const { data } = await calendar.events.list(params);

      if (!data.items || data.items.length === 0) {
        return [];
      }

      // Map response to our EventResponse type
      return data.items.map(event => ({
        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
      }));
    } catch (error) {
      throw this.handleError(error, 'Failed to get events');
    }
  }

  /**
   * Retrieve a single calendar event by ID
   */
  async getEvent(email: string, eventId: string): Promise<EventResponse> {
    try {
      const calendar = await this.getCalendarClient(email);

      const { data: event } = await calendar.events.get({
        calendarId: 'primary',
        eventId
      });

      if (!event) {
        throw new GoogleServiceError(
          'Event not found',
          'NOT_FOUND',
          `No event found with ID: ${eventId}`
        );
      }

      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
      };
    } catch (error) {
      throw this.handleError(error, 'Failed to get event');
    }
  }

  /**
   * Create a new calendar event
   */
  async createEvent({ email, summary, description, start, end, attendees }: CreateEventParams): Promise<CreateEventResponse> {
    try {
      const calendar = await this.getCalendarClient(email);

      const eventData = {
        summary,
        description,
        start,
        end,
        attendees: attendees?.map(attendee => ({ email: attendee.email }))
      };

      const { data: event } = await calendar.events.insert({
        calendarId: 'primary',
        requestBody: eventData,
        sendUpdates: 'all'  // Send emails to attendees
      });

      if (!event.id || !event.summary) {
        throw new GoogleServiceError(
          'Failed to create event',
          'CREATE_ERROR',
          'Event creation response was incomplete'
        );
      }

      return {
        id: event.id,
        summary: event.summary,
        htmlLink: event.htmlLink || ''
      };
    } catch (error) {
      throw this.handleError(error, 'Failed to create event');
    }
  }
}

```

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

```typescript
import { google } from 'googleapis';
import { BaseGoogleService } from '../../../services/base/BaseGoogleService.js';
import {
  GetEmailsParams,
  SendEmailParams,
  SendEmailResponse,
  GetGmailSettingsParams,
  GetGmailSettingsResponse,
  GmailError,
  GmailModuleConfig,
  GetEmailsResponse,
  DraftResponse,
  GetDraftsResponse,
  Label,
  GetLabelsResponse,
  GetLabelFiltersResponse,
  LabelFilter
} from '../types.js';
import { AttachmentIndexService } from '../../attachments/index-service.js';

import {
  ManageLabelParams,
  ManageLabelAssignmentParams,
  ManageLabelFilterParams
} from './label.js';
import { ManageDraftParams } from './draft.js';
import { EmailService } from './email.js';
import { SearchService } from './search.js';
import { DraftService } from './draft.js';
import { SettingsService } from './settings.js';
import { LabelService } from './label.js';
import { GmailAttachmentService } from './attachment.js';

/**
 * Gmail service implementation extending BaseGoogleService for common auth handling.
 */
export class GmailService extends BaseGoogleService<ReturnType<typeof google.gmail>> {
  private emailService: EmailService;
  private searchService: SearchService;
  private draftService: DraftService;
  private settingsService: SettingsService;
  private labelService: LabelService;
  private attachmentService: GmailAttachmentService;
  private initialized = false;
  
  constructor(config?: GmailModuleConfig) {
    super({ serviceName: 'Gmail', version: 'v1' });
    
    this.searchService = new SearchService();
    this.attachmentService = GmailAttachmentService.getInstance();
    this.emailService = new EmailService(this.searchService, this.attachmentService);
    this.draftService = new DraftService(this.attachmentService);
    this.settingsService = new SettingsService();
    this.labelService = new LabelService();
  }

  private async ensureInitialized(email: string) {
    if (!this.initialized) {
      await this.initialize();
    }
    await this.getGmailClient(email);
  }

  /**
   * Initialize the Gmail service
   */
  public async initialize(): Promise<void> {
    try {
      await super.initialize();
      this.initialized = true;
    } catch (error) {
      throw new GmailError(
        'Failed to initialize Gmail service',
        'INIT_ERROR',
        error instanceof Error ? error.message : 'Unknown error'
      );
    }
  }

  /**
   * Ensures all services are properly initialized
   */
  private checkInitialized() {
    if (!this.initialized) {
      throw new GmailError(
        'Gmail service not initialized',
        'INIT_ERROR',
        'Please call init() before using the service'
      );
    }
  }

  /**
   * Gets an authenticated Gmail client for the specified account.
   */
  private async getGmailClient(email: string) {
    if (!this.initialized) {
      await this.initialize();
    }
    
    return this.getAuthenticatedClient(
      email,
      (auth) => {
        const client = google.gmail({ version: 'v1', auth });
        
        // Update service instances with new client
        this.emailService.updateClient(client);
        this.draftService.updateClient(client);
        this.settingsService.updateClient(client);
        this.labelService.updateClient(client);
        this.attachmentService.updateClient(client);
        
        return client;
      }
    );
  }

  async getEmails(params: GetEmailsParams): Promise<GetEmailsResponse> {
    await this.getGmailClient(params.email);
    return this.emailService.getEmails(params);
  }

  async sendEmail(params: SendEmailParams): Promise<SendEmailResponse> {
    await this.getGmailClient(params.email);
    return this.emailService.sendEmail(params);
  }

  async manageDraft(params: ManageDraftParams): Promise<DraftResponse | GetDraftsResponse | SendEmailResponse | void> {
    await this.getGmailClient(params.email);
    return this.draftService.manageDraft(params);
  }

  async getWorkspaceGmailSettings(params: GetGmailSettingsParams): Promise<GetGmailSettingsResponse> {
    await this.getGmailClient(params.email);
    return this.settingsService.getWorkspaceGmailSettings(params);
  }

  // Consolidated Label Management Methods
  async manageLabel(params: ManageLabelParams): Promise<Label | GetLabelsResponse | void> {
    await this.getGmailClient(params.email);
    return this.labelService.manageLabel(params);
  }

  async manageLabelAssignment(params: ManageLabelAssignmentParams): Promise<void> {
    await this.getGmailClient(params.email);
    return this.labelService.manageLabelAssignment(params);
  }

  async manageLabelFilter(params: ManageLabelFilterParams): Promise<LabelFilter | GetLabelFiltersResponse | void> {
    await this.getGmailClient(params.email);
    return this.labelService.manageLabelFilter(params);
  }

  async getAttachment(email: string, messageId: string, filename: string) {
    await this.ensureInitialized(email);
    return this.attachmentService.getAttachment(email, messageId, filename);
  }
}

```

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

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

interface ServiceConfig {
  version: string;
  methods: string[];
  scopes: Record<string, string[]>;
}

export class EndpointValidator {
  // Registry of supported services and their configurations
  private readonly serviceRegistry: Record<string, ServiceConfig> = {
    calendar: {
      version: 'v3',
      methods: [
        'events.list',
        'events.get',
        'events.insert',
        'events.update',
        'events.delete',
        'events.attachments.get',
        'events.attachments.upload',
        'events.attachments.delete'
      ],
      scopes: {
        'events.list': ['https://www.googleapis.com/auth/calendar.readonly'],
        'events.get': ['https://www.googleapis.com/auth/calendar.readonly'],
        'events.insert': ['https://www.googleapis.com/auth/calendar.events'],
        'events.update': ['https://www.googleapis.com/auth/calendar.events'],
        'events.delete': ['https://www.googleapis.com/auth/calendar.events'],
        'events.attachments.get': ['https://www.googleapis.com/auth/calendar.readonly'],
        'events.attachments.upload': ['https://www.googleapis.com/auth/calendar.events'],
        'events.attachments.delete': ['https://www.googleapis.com/auth/calendar.events']
      }
    },
    gmail: {
      version: 'v1',
      methods: [
        'users.messages.list',
        'users.messages.get',
        'users.messages.send',
        'users.labels.list',
        'users.labels.get',
        'users.drafts.list',
        'users.drafts.get',
        'users.drafts.create',
        'users.drafts.update',
        'users.drafts.delete'
      ],
      scopes: {
        'users.messages.list': ['https://www.googleapis.com/auth/gmail.readonly'],
        'users.messages.get': ['https://www.googleapis.com/auth/gmail.readonly'],
        'users.messages.send': ['https://www.googleapis.com/auth/gmail.send'],
        'users.labels.list': ['https://www.googleapis.com/auth/gmail.labels'],
        'users.labels.get': ['https://www.googleapis.com/auth/gmail.labels'],
        'users.drafts.list': ['https://www.googleapis.com/auth/gmail.readonly'],
        'users.drafts.get': ['https://www.googleapis.com/auth/gmail.readonly'],
        'users.drafts.create': ['https://www.googleapis.com/auth/gmail.compose'],
        'users.drafts.update': ['https://www.googleapis.com/auth/gmail.compose'],
        'users.drafts.delete': ['https://www.googleapis.com/auth/gmail.compose']
      }
    },
    drive: {
      version: 'v3',
      methods: [
        'files.list',
        'files.get',
        'files.create',
        'files.update',
        'files.delete',
        'files.copy',
        'permissions.list',
        'permissions.get',
        'permissions.create',
        'permissions.update',
        'permissions.delete'
      ],
      scopes: {
        'files.list': ['https://www.googleapis.com/auth/drive.readonly'],
        'files.get': ['https://www.googleapis.com/auth/drive.readonly'],
        'files.create': ['https://www.googleapis.com/auth/drive.file'],
        'files.update': ['https://www.googleapis.com/auth/drive.file'],
        'files.delete': ['https://www.googleapis.com/auth/drive.file'],
        'files.copy': ['https://www.googleapis.com/auth/drive.file'],
        'permissions.list': ['https://www.googleapis.com/auth/drive.readonly'],
        'permissions.get': ['https://www.googleapis.com/auth/drive.readonly'],
        'permissions.create': ['https://www.googleapis.com/auth/drive.file'],
        'permissions.update': ['https://www.googleapis.com/auth/drive.file'],
        'permissions.delete': ['https://www.googleapis.com/auth/drive.file']
      }
    }
  };

  async validate(endpoint: string): Promise<void> {
    // Parse endpoint into service and method
    const [service, ...methodParts] = endpoint.split('.');
    const methodName = methodParts.join('.');

    // Validate service exists
    if (!service || !this.serviceRegistry[service]) {
      throw new GoogleApiError(
        `Service "${service}" is not supported`,
        'INVALID_SERVICE',
        `Supported services are: ${Object.keys(this.serviceRegistry).join(', ')}`
      );
    }

    // Validate method exists
    const serviceConfig = this.serviceRegistry[service];
    if (!methodName || !serviceConfig.methods.includes(methodName)) {
      throw new GoogleApiError(
        `Method "${methodName}" is not supported for service "${service}"`,
        'INVALID_METHOD',
        `Available methods for ${service} are: ${serviceConfig.methods.join(', ')}`
      );
    }
  }

  getRequiredScopes(endpoint: string): string[] {
    const [service, ...methodParts] = endpoint.split('.');
    const methodName = methodParts.join('.');

    const serviceConfig = this.serviceRegistry[service];
    if (!serviceConfig || !serviceConfig.scopes[methodName]) {
      return [];
    }

    return serviceConfig.scopes[methodName];
  }

  getSupportedServices(): string[] {
    return Object.keys(this.serviceRegistry);
  }

  getSupportedMethods(service: string): string[] {
    return this.serviceRegistry[service]?.methods || [];
  }
}

```

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

```typescript
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { getAccountManager } from '../modules/accounts/index.js';
import { getDriveService } from '../modules/drive/index.js';
import { FileListOptions, FileSearchOptions, FileUploadOptions, PermissionOptions } from '../modules/drive/types.js';
import { McpToolResponse } from './types.js';

interface DriveFileListArgs {
  email: string;
  options?: FileListOptions;
}

interface DriveSearchArgs {
  email: string;
  options: FileSearchOptions;
}

interface DriveUploadArgs {
  email: string;
  options: FileUploadOptions;
}

interface DriveDownloadArgs {
  email: string;
  fileId: string;
  mimeType?: string;
}

interface DriveFolderArgs {
  email: string;
  name: string;
  parentId?: string;
}

interface DrivePermissionArgs {
  email: string;
  options: PermissionOptions;
}

interface DriveDeleteArgs {
  email: string;
  fileId: string;
}

export async function handleListDriveFiles(args: DriveFileListArgs): Promise<McpToolResponse> {
  const accountManager = getAccountManager();
  
  return await accountManager.withTokenRenewal(args.email, async () => {
    const driveService = await getDriveService();
    const result = await driveService.listFiles(args.email, args.options || {});
    return {
      content: [{
        type: 'text',
        text: JSON.stringify(result, null, 2)
      }]
    };
  });
}

export async function handleSearchDriveFiles(args: DriveSearchArgs): Promise<McpToolResponse> {
  const accountManager = getAccountManager();
  
  return await accountManager.withTokenRenewal(args.email, async () => {
    const driveService = await getDriveService();
    const result = await driveService.searchFiles(args.email, args.options);
    return {
      content: [{
        type: 'text',
        text: JSON.stringify(result, null, 2)
      }]
    };
  });
}

export async function handleUploadDriveFile(args: DriveUploadArgs): Promise<McpToolResponse> {
  if (!args.options.name) {
    throw new McpError(ErrorCode.InvalidParams, 'File name is required');
  }
  if (!args.options.content) {
    throw new McpError(ErrorCode.InvalidParams, 'File content is required');
  }

  const accountManager = getAccountManager();
  
  return await accountManager.withTokenRenewal(args.email, async () => {
    const driveService = await getDriveService();
    const result = await driveService.uploadFile(args.email, args.options);
    return {
      content: [{
        type: 'text',
        text: JSON.stringify(result, null, 2)
      }]
    };
  });
}

export async function handleDownloadDriveFile(args: DriveDownloadArgs): Promise<McpToolResponse> {
  if (!args.fileId) {
    throw new McpError(ErrorCode.InvalidParams, 'File ID is required');
  }

  const accountManager = getAccountManager();
  
  return await accountManager.withTokenRenewal(args.email, async () => {
    const driveService = await getDriveService();
    const result = await driveService.downloadFile(args.email, {
      fileId: args.fileId,
      mimeType: args.mimeType
    });
    return {
      content: [{
        type: 'text',
        text: JSON.stringify(result, null, 2)
      }]
    };
  });
}

export async function handleCreateDriveFolder(args: DriveFolderArgs): Promise<McpToolResponse> {
  if (!args.name) {
    throw new McpError(ErrorCode.InvalidParams, 'Folder name is required');
  }

  const accountManager = getAccountManager();
  
  return await accountManager.withTokenRenewal(args.email, async () => {
    const driveService = await getDriveService();
    const result = await driveService.createFolder(args.email, args.name, args.parentId);
    return {
      content: [{
        type: 'text',
        text: JSON.stringify(result, null, 2)
      }]
    };
  });
}

export async function handleUpdateDrivePermissions(args: DrivePermissionArgs): Promise<McpToolResponse> {
  if (!args.options.fileId) {
    throw new McpError(ErrorCode.InvalidParams, 'File ID is required');
  }
  if (!args.options.role) {
    throw new McpError(ErrorCode.InvalidParams, 'Permission role is required');
  }
  if (!args.options.type) {
    throw new McpError(ErrorCode.InvalidParams, 'Permission type is required');
  }

  const accountManager = getAccountManager();
  
  return await accountManager.withTokenRenewal(args.email, async () => {
    const driveService = await getDriveService();
    const result = await driveService.updatePermissions(args.email, args.options);
    return {
      content: [{
        type: 'text',
        text: JSON.stringify(result, null, 2)
      }]
    };
  });
}

export async function handleDeleteDriveFile(args: DriveDeleteArgs): Promise<McpToolResponse> {
  if (!args.fileId) {
    throw new McpError(ErrorCode.InvalidParams, 'File ID is required');
  }

  const accountManager = getAccountManager();
  
  return await accountManager.withTokenRenewal(args.email, async () => {
    const driveService = await getDriveService();
    const result = await driveService.deleteFile(args.email, args.fileId);
    return {
      content: [{
        type: 'text',
        text: JSON.stringify(result, null, 2)
      }]
    };
  });
}

```

--------------------------------------------------------------------------------
/docs/ERRORS.md:
--------------------------------------------------------------------------------

```markdown
# Error Handling Documentation

## Error Response Format

All errors follow a standardized format:
```typescript
{
  status: "error",
  error: string,      // Human-readable error message
  resolution: string  // Suggested steps to resolve the error
}
```

## Error Categories

### 1. Validation Errors

#### INVALID_SERVICE
- **Description**: The requested Google API service is not supported
- **Possible Causes**:
  - Service name is misspelled
  - Service is not implemented in the server
- **Resolution**: Check supported services in API documentation

#### INVALID_METHOD
- **Description**: The requested API method is not supported
- **Possible Causes**:
  - Method name is misspelled
  - Method is not implemented for the service
- **Resolution**: Verify method name in API documentation

#### INVALID_ENDPOINT
- **Description**: Malformed API endpoint format
- **Possible Causes**:
  - Incorrect endpoint format
  - Missing service or method parts
- **Resolution**: Use format "service.method" (e.g., "drive.files.list")

#### MISSING_REQUIRED_PARAMS
- **Description**: Required parameters are missing from the request
- **Possible Causes**:
  - Required parameters not provided
  - Parameters misspelled
- **Resolution**: Check required parameters in API documentation

#### INVALID_PARAM_TYPE
- **Description**: Parameter value type doesn't match expected type
- **Possible Causes**:
  - Wrong data type provided
  - Array provided instead of string or vice versa
- **Resolution**: Verify parameter types in API documentation

### 2. Authentication Errors

#### TOKEN_EXPIRED
- **Description**: OAuth token has expired
- **Possible Causes**:
  - Token age exceeds expiration time
  - Token invalidated by user
- **Resolution**: Server will automatically refresh token, retry request

#### TOKEN_INVALID
- **Description**: OAuth token is invalid
- **Possible Causes**:
  - Token revoked
  - Token malformed
- **Resolution**: Please authenticate the account, which will grant all necessary permissions

#### INSUFFICIENT_SCOPE
- **Description**: Account needs authentication
- **Possible Causes**:
  - Account not authenticated
  - New permissions needed
- **Resolution**: Please authenticate the account, which will grant all necessary permissions

### 3. API Errors

#### API_ERROR_400
- **Description**: Bad Request
- **Possible Causes**:
  - Invalid request parameters
  - Malformed request body
- **Resolution**: Check request format and parameters

#### API_ERROR_401
- **Description**: Unauthorized
- **Possible Causes**:
  - Invalid credentials
  - Token expired
- **Resolution**: Check authentication status

#### API_ERROR_403
- **Description**: Forbidden
- **Possible Causes**:
  - Insufficient permissions
  - Account restrictions
- **Resolution**: Please authenticate the account, which will grant all necessary permissions

#### API_ERROR_404
- **Description**: Resource Not Found
- **Possible Causes**:
  - Invalid resource ID
  - Resource deleted
- **Resolution**: Verify resource exists

#### API_ERROR_429
- **Description**: Rate Limit Exceeded
- **Possible Causes**:
  - Too many requests
  - Quota exceeded
- **Resolution**: Implement exponential backoff

#### API_ERROR_500
- **Description**: Internal Server Error
- **Possible Causes**:
  - Google API error
  - Server-side issue
- **Resolution**: Retry request later

#### API_ERROR_503
- **Description**: Service Unavailable
- **Possible Causes**:
  - Google API maintenance
  - Temporary outage
- **Resolution**: Retry request later

### 4. System Errors

#### SERVICE_INIT_FAILED
- **Description**: Failed to initialize Google service
- **Possible Causes**:
  - Configuration issues
  - Network problems
- **Resolution**: Check server configuration

#### NETWORK_ERROR
- **Description**: Network communication failed
- **Possible Causes**:
  - Connection issues
  - Timeout
- **Resolution**: Check network connection and retry

## Error Handling Best Practices

1. **Always Check Status**
   - Verify response status before processing
   - Handle both success and error cases

2. **Implement Retry Logic**
   - Retry on transient errors (429, 500, 503)
   - Use exponential backoff
   - Set maximum retry attempts

3. **Handle Authentication Flows**
   - Watch for token expiration
   - Handle refresh token flow
   - Re-authenticate when needed

4. **Log Errors Appropriately**
   - Include error codes and messages
   - Log stack traces for debugging
   - Don't log sensitive information

5. **User Communication**
   - Provide clear, user-friendly error messages
   - Include simple resolution steps
   - Avoid technical jargon in user-facing messages
   - Offer support contact if needed

## Example Error Handling

```typescript
try {
  const response = await makeRequest();
  if (response.status === "error") {
    // Handle error based on type
    switch (response.error) {
      case "TOKEN_EXPIRED":
        // Handle token refresh
        break;
      case "INVALID_PARAM_TYPE":
        // Fix parameters and retry
        break;
      // ... handle other cases
    }
  }
} catch (error) {
  // Handle unexpected errors
  console.error("Request failed:", error);
}

```
Page 1/3FirstPrevNextLast