#
tokens: 49753/50000 84/127 files (page 1/4)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 4. Use http://codebase.md/aaronsb/google-workspace-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .dockerignore
├── .eslintrc.json
├── .github
│   ├── config.yml
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── pull_request_template.md
│   └── workflows
│       ├── ci.yml
│       └── docker-publish.yml
├── .gitignore
├── ARCHITECTURE.md
├── cline_docs
│   ├── activeContext.md
│   ├── productContext.md
│   ├── progress.md
│   ├── systemPatterns.md
│   └── techContext.md
├── CODE_OF_CONDUCT.md
├── config
│   ├── accounts.example.json
│   ├── credentials
│   │   └── README.md
│   └── gauth.example.json
├── CONTRIBUTING.md
├── docker-entrypoint.sh
├── Dockerfile
├── Dockerfile.local
├── docs
│   ├── API.md
│   ├── assets
│   │   └── robot-assistant.png
│   ├── automatic-oauth-flow.md
│   ├── ERRORS.md
│   ├── EXAMPLES.md
│   └── TOOL_DISCOVERY.md
├── jest.config.cjs
├── jest.setup.cjs
├── LICENSE
├── llms-install.md
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   ├── build-local.sh
│   └── local-entrypoint.sh
├── SECURITY.md
├── smithery.yaml
├── src
│   ├── __fixtures__
│   │   └── accounts.ts
│   ├── __helpers__
│   │   ├── package.json
│   │   └── testSetup.ts
│   ├── __mocks__
│   │   ├── @modelcontextprotocol
│   │   │   ├── sdk
│   │   │   │   ├── server
│   │   │   │   │   ├── index.js
│   │   │   │   │   └── stdio.js
│   │   │   │   └── types.ts
│   │   │   └── sdk.ts
│   │   ├── googleapis.ts
│   │   └── logger.ts
│   ├── __tests__
│   │   └── modules
│   │       ├── accounts
│   │       │   ├── manager.test.ts
│   │       │   └── token.test.ts
│   │       ├── attachments
│   │       │   └── index.test.ts
│   │       ├── calendar
│   │       │   └── service.test.ts
│   │       └── gmail
│   │           └── service.test.ts
│   ├── api
│   │   ├── handler.ts
│   │   ├── request.ts
│   │   └── validators
│   │       ├── endpoint.ts
│   │       └── parameter.ts
│   ├── index.ts
│   ├── modules
│   │   ├── accounts
│   │   │   ├── callback-server.ts
│   │   │   ├── index.ts
│   │   │   ├── manager.ts
│   │   │   ├── oauth.ts
│   │   │   ├── token.ts
│   │   │   └── types.ts
│   │   ├── attachments
│   │   │   ├── cleanup-service.ts
│   │   │   ├── index-service.ts
│   │   │   ├── response-transformer.ts
│   │   │   ├── service.ts
│   │   │   ├── transformer.ts
│   │   │   └── types.ts
│   │   ├── calendar
│   │   │   ├── __tests__
│   │   │   │   └── scopes.test.ts
│   │   │   ├── index.ts
│   │   │   ├── scopes.ts
│   │   │   ├── service.ts
│   │   │   └── types.ts
│   │   ├── contacts
│   │   │   ├── index.ts
│   │   │   ├── scopes.ts
│   │   │   └── types.ts
│   │   ├── drive
│   │   │   ├── __tests__
│   │   │   │   ├── scopes.test.ts
│   │   │   │   └── service.test.ts
│   │   │   ├── index.ts
│   │   │   ├── scopes.ts
│   │   │   ├── service.ts
│   │   │   └── types.ts
│   │   ├── gmail
│   │   │   ├── __tests__
│   │   │   │   ├── label.test.ts
│   │   │   │   └── scopes.test.ts
│   │   │   ├── constants.ts
│   │   │   ├── index.ts
│   │   │   ├── scopes.ts
│   │   │   ├── service.ts
│   │   │   ├── services
│   │   │   │   ├── attachment.ts
│   │   │   │   ├── base.ts
│   │   │   │   ├── draft.ts
│   │   │   │   ├── email.ts
│   │   │   │   ├── label.ts
│   │   │   │   ├── search.ts
│   │   │   │   └── settings.ts
│   │   │   └── types.ts
│   │   └── tools
│   │       ├── __tests__
│   │       │   ├── registry.test.ts
│   │       │   └── scope-registry.test.ts
│   │       ├── registry.ts
│   │       └── scope-registry.ts
│   ├── oauth
│   │   └── client.ts
│   ├── scripts
│   │   ├── health-check.ts
│   │   ├── setup-environment.ts
│   │   └── setup-google-env.ts
│   ├── services
│   │   ├── base
│   │   │   └── BaseGoogleService.ts
│   │   ├── calendar
│   │   │   └── index.ts
│   │   ├── contacts
│   │   │   └── index.ts
│   │   ├── drive
│   │   │   └── index.ts
│   │   └── gmail
│   │       └── index.ts
│   ├── tools
│   │   ├── account-handlers.ts
│   │   ├── calendar-handlers.ts
│   │   ├── contacts-handlers.ts
│   │   ├── definitions.ts
│   │   ├── drive-handlers.ts
│   │   ├── gmail-handlers.ts
│   │   ├── server.ts
│   │   ├── type-guards.ts
│   │   └── types.ts
│   ├── types
│   │   └── modelcontextprotocol__sdk.d.ts
│   ├── types.ts
│   └── utils
│       ├── account.ts
│       ├── logger.ts
│       ├── service-initializer.ts
│       ├── token.ts
│       └── workspace.ts
├── TODO.md
└── tsconfig.json
```

# Files

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

```
 1 | # Dependencies
 2 | node_modules/
 3 | npm-debug.log*
 4 | yarn-debug.log*
 5 | yarn-error.log*
 6 | 
 7 | # Build output
 8 | build/
 9 | dist/
10 | *.tsbuildinfo
11 | 
12 | # Environment
13 | .env
14 | .env.*
15 | 
16 | # Credentials
17 | config/gauth.json
18 | config/accounts.json
19 | config/credentials/
20 | *.token.json
21 | 
22 | # IDE
23 | .idea/
24 | .vscode/
25 | *.swp
26 | *.swo
27 | 
28 | # OS
29 | .DS_Store
30 | Thumbs.db
31 | 
32 | # Logs
33 | logs/
34 | *.log
35 | 
36 | # Test coverage
37 | coverage/
38 | commit-message.txt
39 | 
```

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

```
 1 | # Version control
 2 | .git
 3 | .gitignore
 4 | .github
 5 | 
 6 | # Dependencies
 7 | node_modules
 8 | npm-debug.log*
 9 | .npm
10 | .npmrc
11 | 
12 | # Development files
13 | .vscode
14 | .idea
15 | *.log
16 | logs
17 | coverage
18 | .nyc_output
19 | *.tsbuildinfo
20 | 
21 | # Documentation and markdown
22 | *.md
23 | !README.md
24 | docs
25 | cline_docs
26 | ARCHITECTURE.md
27 | CODE_OF_CONDUCT.md
28 | CONTRIBUTING.md
29 | LICENSE
30 | SECURITY.md
31 | TODO.md
32 | 
33 | # Test files
34 | __tests__
35 | __fixtures__
36 | __helpers__
37 | __mocks__
38 | *.test.*
39 | *.spec.*
40 | jest.config.*
41 | jest.setup.*
42 | test
43 | tests
44 | 
45 | # Configuration and build files
46 | .eslintrc*
47 | .prettierrc*
48 | .editorconfig
49 | *.config.js
50 | *.config.cjs
51 | *.config.mjs
52 | config/*
53 | !config/*.example.json
54 | 
55 | # Source maps
56 | *.map
57 | 
58 | # Build artifacts
59 | dist
60 | build
61 | coverage
62 | .nyc_output
63 | 
64 | # Environment files
65 | .env*
66 | .dockerenv
67 | 
68 | # Docker
69 | .docker
70 | docker-compose*
71 | Dockerfile*
72 | .dockerignore
73 | 
74 | # Temporary files
75 | *.swp
76 | *.swo
77 | .DS_Store
78 | Thumbs.db
79 | 
```

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

```json
 1 | {
 2 |   "parser": "@typescript-eslint/parser",
 3 |   "plugins": ["@typescript-eslint"],
 4 |   "extends": [
 5 |     "eslint:recommended",
 6 |     "plugin:@typescript-eslint/recommended"
 7 |   ],
 8 |   "env": {
 9 |     "node": true,
10 |     "es6": true
11 |   },
12 |   "parserOptions": {
13 |     "ecmaVersion": 2020,
14 |     "sourceType": "module"
15 |   },
16 |   "rules": {
17 |     "@typescript-eslint/explicit-function-return-type": "off",
18 |     "@typescript-eslint/no-explicit-any": "off",
19 |     "@typescript-eslint/no-unused-vars": ["warn", { 
20 |       "argsIgnorePattern": "^_|^method$|^config$|^email$|^extra$",
21 |       "varsIgnorePattern": "^_"
22 |     }],
23 |     "@typescript-eslint/no-var-requires": "off"
24 |   },
25 |   "overrides": [
26 |     {
27 |       "files": ["**/__tests__/**/*", "**/__mocks__/**/*", "**/__fixtures__/**/*"],
28 |       "rules": {
29 |         "@typescript-eslint/no-unused-vars": "off"
30 |       }
31 |     }
32 |   ]
33 | }
34 | 
```

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

```markdown
1 | # Credentials Directory
2 | 
3 | This directory is used to store OAuth token files for authenticated Google accounts.
4 | Token files are automatically created and managed by the application.
5 | 
6 | Note: The contents of this directory should not be committed to version control.
7 | Token files are stored with the pattern: `[sanitized-email].token.json`
8 | 
```

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

```markdown
  1 | # Google Workspace MCP Server
  2 | 
  3 | ![Robot Assistant](https://raw.githubusercontent.com/aaronsb/google-workspace-mcp/main/docs/assets/robot-assistant.png)
  4 | 
  5 | 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.
  6 | 
  7 | 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.
  8 | 
  9 | 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.
 10 | 
 11 | 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.
 12 | 
 13 | ## Key Features
 14 | 
 15 | - **Gmail Management**: Search, send, organize emails with advanced filtering and label management
 16 | - **Calendar Operations**: Create, update, and manage events with full scheduling capabilities
 17 | - **Drive Integration**: Upload, download, search, and manage files with permission controls
 18 | - **Contact Access**: Retrieve and manage your Google contacts
 19 | - **Secure Authentication**: OAuth 2.0 flow with automatic token refresh
 20 | - **Multi-Account Support**: Manage multiple Google accounts simultaneously
 21 | 
 22 | ## Quick Start
 23 | 
 24 | ### Prerequisites
 25 | 
 26 | 1. **Google Cloud Project Setup**:
 27 |    - Create a project in [Google Cloud Console](https://console.cloud.google.com)
 28 |    - Enable Gmail API, Calendar API, and Drive API
 29 |    - Configure OAuth consent screen as "External"
 30 |    - Add yourself as a test user
 31 | 
 32 | 2. **OAuth Credentials**:
 33 |    - Create OAuth 2.0 credentials
 34 |    - Choose "Web application" type
 35 |    - Set redirect URI to: `http://localhost:8080`
 36 |    - Save your Client ID and Client Secret
 37 | 
 38 | 3. **Local Setup**:
 39 |    - Install Docker
 40 |    - Create config directory: `mkdir -p ~/.mcp/google-workspace-mcp`
 41 |        - If the directory already exists, ensure your user owns it
 42 | 
 43 | ### Configuration
 44 | 
 45 | Add the server to your MCP client configuration:
 46 | 
 47 | **For Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
 48 | ```json
 49 | {
 50 |   "mcpServers": {
 51 |     "google-workspace-mcp": {
 52 |       "command": "docker",
 53 |       "args": [
 54 |         "run",
 55 |         "--rm",
 56 |         "-i",
 57 |         "-p", "8080:8080",
 58 |         "-v", "~/.mcp/google-workspace-mcp:/app/config",
 59 |         "-v", "~/Documents/workspace-mcp-files:/app/workspace",
 60 |         "-e", "GOOGLE_CLIENT_ID",
 61 |         "-e", "GOOGLE_CLIENT_SECRET",
 62 |         "-e", "LOG_MODE=strict",
 63 |         "ghcr.io/aaronsb/google-workspace-mcp:latest"
 64 |       ],
 65 |       "env": {
 66 |         "GOOGLE_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
 67 |         "GOOGLE_CLIENT_SECRET": "your-client-secret"
 68 |       }
 69 |     }
 70 |   }
 71 | }
 72 | ```
 73 | 
 74 | **For Cline** (`~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`):
 75 | ```json
 76 | {
 77 |   "mcpServers": {
 78 |     "google-workspace-mcp": {
 79 |       "command": "docker",
 80 |       "args": [
 81 |         "run",
 82 |         "--rm",
 83 |         "-i",
 84 |         "-p", "8080:8080",
 85 |         "-v", "~/.mcp/google-workspace-mcp:/app/config",
 86 |         "-v", "~/Documents/workspace-mcp-files:/app/workspace",
 87 |         "-e", "GOOGLE_CLIENT_ID",
 88 |         "-e", "GOOGLE_CLIENT_SECRET",
 89 |         "-e", "LOG_MODE=strict",
 90 |         "ghcr.io/aaronsb/google-workspace-mcp:latest"
 91 |       ],
 92 |       "env": {
 93 |         "GOOGLE_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
 94 |         "GOOGLE_CLIENT_SECRET": "your-client-secret"
 95 |       }
 96 |     }
 97 |   }
 98 | }
 99 | ```
100 | 
101 | **Key Configuration Notes**:
102 | - Port mapping `-p 8080:8080` is required for OAuth callback handling
103 | - Replace placeholder credentials with your actual Google Cloud OAuth credentials
104 | - The `LOG_MODE=strict` setting is recommended but not required
105 | 
106 | Logging modes:
107 | - normal (default): Uses appropriate console methods for each log level
108 | - strict: Routes all non-JSON-RPC messages to stderr
109 | 
110 | ### Authentication
111 | 
112 | 1. Restart your MCP client after configuration
113 | 2. Ask your AI assistant to "add my Google account"
114 | 3. Follow the OAuth flow:
115 |    - Click the provided authorization URL
116 |    - Sign in to Google and grant permissions
117 |    - Copy the authorization code from the success page
118 |    - Provide the code back to complete authentication
119 | 
120 | ## Architecture
121 | 
122 | ### OAuth Flow
123 | 
124 | The server implements a secure OAuth 2.0 flow:
125 | 
126 | 1. **Callback Server**: Automatically starts on `localhost:8080` to handle OAuth redirects
127 | 2. **Authorization**: Generates Google OAuth URLs for user authentication
128 | 3. **Token Management**: Securely stores and automatically refreshes access tokens
129 | 4. **Multi-Account**: Supports multiple Google accounts with isolated token storage
130 | 
131 | ### File Management
132 | 
133 | Files are organized in a structured workspace:
134 | 
135 | ```
136 | ~/Documents/workspace-mcp-files/
137 | ├── [[email protected]]/
138 | │   ├── downloads/        # Files downloaded from Drive
139 | │   └── uploads/         # Files staged for upload
140 | ├── [[email protected]]/
141 | │   ├── downloads/
142 | │   └── uploads/
143 | └── shared/
144 |     └── temp/           # Temporary files (auto-cleanup)
145 | ```
146 | 
147 | ## Available Tools
148 | 
149 | ### Account Management
150 | - `list_workspace_accounts` - List configured accounts and authentication status
151 | - `authenticate_workspace_account` - Add and authenticate Google accounts
152 | - `remove_workspace_account` - Remove accounts and associated tokens
153 | 
154 | ### Gmail Operations
155 | - `search_workspace_emails` - Advanced email search with filtering
156 | - `send_workspace_email` - Send emails with attachments and formatting
157 | - `manage_workspace_draft` - Create, update, and manage email drafts
158 | - `manage_workspace_label` - Create and manage Gmail labels
159 | - `manage_workspace_label_assignment` - Apply/remove labels from messages
160 | - `manage_workspace_label_filter` - Create automated label filters
161 | - `get_workspace_gmail_settings` - Access Gmail account settings
162 | 
163 | ### Calendar Operations
164 | - `list_workspace_calendar_events` - List and search calendar events
165 | - `get_workspace_calendar_event` - Get detailed event information
166 | - `create_workspace_calendar_event` - Create new events with attendees
167 | - `manage_workspace_calendar_event` - Update events and respond to invitations
168 | - `delete_workspace_calendar_event` - Delete calendar events
169 | 
170 | ### Drive Operations
171 | - `list_drive_files` - List files with filtering and pagination
172 | - `search_drive_files` - Full-text search across Drive content
173 | - `upload_drive_file` - Upload files with metadata and permissions
174 | - `download_drive_file` - Download files with format conversion
175 | - `delete_drive_file` - Delete files and folders
176 | - `create_drive_folder` - Create organized folder structures
177 | - `update_drive_permissions` - Manage file sharing and permissions
178 | 
179 | ### Contacts Operations
180 | - `get_workspace_contacts` - Retrieve contact information and details
181 | 
182 | See [API Documentation](docs/API.md) for detailed usage examples.
183 | 
184 | ## Development
185 | 
186 | ### Local Development
187 | 
188 | For local development and testing:
189 | 
190 | ```bash
191 | # Clone the repository
192 | git clone https://github.com/aaronsb/google-workspace-mcp.git
193 | cd google-workspace-mcp
194 | 
195 | # Build local Docker image
196 | ./scripts/build-local.sh
197 | 
198 | # Use local image in configuration
199 | # Replace "ghcr.io/aaronsb/google-workspace-mcp:latest" with "google-workspace-mcp:local"
200 | ```
201 | 
202 | ## Troubleshooting
203 | 
204 | ### Common Issues
205 | 
206 | **Authentication Errors**:
207 | - Verify OAuth credentials are correctly configured
208 | - Ensure APIs (Gmail, Calendar, Drive) are enabled in Google Cloud
209 | - Check that you're added as a test user in OAuth consent screen
210 | - Confirm redirect URI is set to `http://localhost:8080`
211 | 
212 | **Connection Issues**:
213 | - Verify port 8080 is available and not blocked by firewall
214 | - Ensure Docker has permission to bind to port 8080
215 | - Check that config directory exists and has proper permissions
216 | 
217 | **Docker Issues**:
218 | macOS:
219 | - Shut down Docker fully from command line with `pkill -SIGHUP -f /Applications/Docker.app 'docker serve'`
220 | - Restart Docker Desktop
221 | - Restart your MCP client (Claude Desktop or Cursor/Cline/etc.)
222 | 
223 | Windows:
224 | - Open Task Manager (Ctrl+Shift+Esc)
225 | - Find and end the "Docker Desktop" process
226 | - Restart Docker Desktop from the Start menu
227 | - Restart your MCP client (Claude Desktop or Cursor/Cline/etc.)
228 | 
229 | **Token Issues**:
230 | - Remove and re-authenticate accounts if tokens become invalid
231 | - Verify API scopes are properly configured in Google Cloud
232 | - Check token expiration and refresh logic
233 | 
234 | ### Getting Help
235 | 
236 | For additional support:
237 | - Check [Error Documentation](docs/ERRORS.md)
238 | - Review [API Examples](docs/EXAMPLES.md)
239 | - Submit issues on GitHub
240 | 
241 | ## Security
242 | 
243 | - OAuth credentials are stored securely in MCP client configuration
244 | - Access tokens are encrypted and stored locally
245 | - Automatic token refresh prevents credential exposure
246 | - Each user maintains their own Google Cloud Project
247 | - No credentials are transmitted to external servers
248 | 
249 | ## License
250 | 
251 | MIT License - See [LICENSE](LICENSE) file for details.
252 | 
253 | ## Contributing
254 | 
255 | Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
256 | 
```

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

```markdown
 1 | # Security Policy
 2 | 
 3 | ## Supported Versions
 4 | 
 5 | | Version | Supported          |
 6 | | ------- | ------------------ |
 7 | | 1.x.x   | :white_check_mark: |
 8 | | < 1.0   | :x:               |
 9 | 
10 | ## Reporting a Vulnerability
11 | 
12 | We take the security of Google Workspace MCP seriously. If you believe you have found a security vulnerability, please follow these steps:
13 | 
14 | 1. **DO NOT** open a public issue on GitHub
15 | 2. Email your findings to [email protected]
16 | 3. Include detailed information about the vulnerability:
17 |    - Description of the issue
18 |    - Steps to reproduce
19 |    - Potential impact
20 |    - Suggested fix (if any)
21 | 
22 | ## What to Expect
23 | 
24 | When you report a vulnerability:
25 | 
26 | 1. You'll receive acknowledgment of your report within 48 hours
27 | 2. We'll investigate and provide an initial assessment within 5 business days
28 | 3. We'll keep you informed about our progress
29 | 4. Once the issue is resolved, we'll notify you and discuss public disclosure
30 | 
31 | ## Security Update Policy
32 | 
33 | 1. Security patches will be released as soon as possible after a vulnerability is confirmed
34 | 2. Updates will be published through:
35 |    - NPM package updates
36 |    - Security advisories on GitHub
37 |    - Release notes in our changelog
38 | 
39 | ## Best Practices
40 | 
41 | When using Google Workspace MCP:
42 | 
43 | 1. Always use the latest version
44 | 2. Keep your OAuth credentials secure
45 | 3. Follow our security guidelines in the documentation
46 | 4. Implement proper access controls
47 | 5. Regularly audit your token usage
48 | 6. Monitor API access logs
49 | 
50 | ## Security Features
51 | 
52 | Google Workspace MCP includes several security features:
53 | 
54 | 1. Secure token storage
55 | 2. OAuth 2.0 implementation
56 | 3. Rate limiting
57 | 4. Input validation
58 | 5. Secure credential handling
59 | 
60 | ## Disclosure Policy
61 | 
62 | - Public disclosure will be coordinated with the reporter
63 | - We aim to release fixes before public disclosure
64 | - Credit will be given to security researchers who report issues (unless they prefer to remain anonymous)
65 | 
66 | ## Security Contact
67 | 
68 | For security-related inquiries, contact:
69 | - Email: [email protected]
70 | - Subject line should start with [SECURITY]
71 | 
```

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

```markdown
  1 | # Contributing to Google Workspace MCP
  2 | 
  3 | Thank you for your interest in contributing to the Google Workspace MCP project! This document provides guidelines and instructions for contributing.
  4 | 
  5 | ## Development Setup
  6 | 
  7 | 1. Fork and clone the repository
  8 | 
  9 | 2. Build the development Docker image:
 10 |    ```bash
 11 |    docker build -t google-workspace-mcp:local .
 12 |    ```
 13 | 
 14 | 3. Create a local config directory:
 15 |    ```bash
 16 |    mkdir -p ~/.mcp/google-workspace-mcp
 17 |    ```
 18 | 
 19 | 4. Set up Google Cloud OAuth credentials:
 20 |    - Create OAuth 2.0 credentials in Google Cloud Console
 21 |    - **Important**: Choose "Web application" type (not Desktop)
 22 |    - Set redirect URI to: `http://localhost:8080`
 23 |    - Note your Client ID and Client Secret
 24 | 
 25 | 5. Run the container with your Google API credentials:
 26 |    ```bash
 27 |    docker run -i --rm \
 28 |      -p 8080:8080 \
 29 |      -v ~/.mcp/google-workspace-mcp:/app/config \
 30 |      -e GOOGLE_CLIENT_ID=your_client_id \
 31 |      -e GOOGLE_CLIENT_SECRET=your_client_secret \
 32 |      -e LOG_MODE=strict \
 33 |      google-workspace-mcp:local
 34 |    ```
 35 | 
 36 | Note: For local development, you can also mount the source code directory:
 37 | ```bash
 38 | docker run -i --rm \
 39 |   -p 8080:8080 \
 40 |   -v ~/.mcp/google-workspace-mcp:/app/config \
 41 |   -v $(pwd)/src:/app/src \
 42 |   -e GOOGLE_CLIENT_ID=your_client_id \
 43 |   -e GOOGLE_CLIENT_SECRET=your_client_secret \
 44 |   -e LOG_MODE=strict \
 45 |   google-workspace-mcp:local
 46 | ```
 47 | 
 48 | **Key Development Notes**:
 49 | - Port mapping `-p 8080:8080` is required for OAuth callback handling
 50 | - OAuth credentials must be "Web application" type with `http://localhost:8080` redirect URI
 51 | - The callback server automatically starts when the OAuth client initializes
 52 | 
 53 | ## Development Workflow
 54 | 
 55 | 1. Create a new branch for your feature/fix:
 56 |    ```bash
 57 |    git checkout -b feature/your-feature-name
 58 |    # or
 59 |    git checkout -b fix/your-fix-name
 60 |    ```
 61 | 
 62 | 2. Make your changes following our coding standards
 63 | 3. Write/update tests as needed
 64 | 4. Build and test your changes:
 65 |    ```bash
 66 |    # Build the development image
 67 |    docker build -t google-workspace-mcp:local .
 68 | 
 69 |    # Run tests in container
 70 |    docker run -i --rm \
 71 |      -v $(pwd):/app \
 72 |      google-workspace-mcp:local \
 73 |      npm test
 74 |    ```
 75 | 5. Commit your changes using conventional commit messages:
 76 |    ```
 77 |    feat: add new feature
 78 |    fix: resolve specific issue
 79 |    docs: update documentation
 80 |    test: add/update tests
 81 |    refactor: code improvements
 82 |    ```
 83 | 
 84 | ## Coding Standards
 85 | 
 86 | - Use TypeScript for all new code
 87 | - Follow existing code style and formatting
 88 | - Maintain 100% test coverage for new code
 89 | - Document all public APIs using JSDoc comments
 90 | - Use meaningful variable and function names
 91 | - Keep functions focused and modular
 92 | - Add comments for complex logic
 93 | 
 94 | ## Testing Requirements
 95 | 
 96 | - Write unit tests for all new functionality
 97 | - Use Jest for testing
 98 | - Mock external dependencies
 99 | - Test both success and error cases
100 | - Maintain existing test coverage
101 | - Run the full test suite before submitting PR
102 | 
103 | ## Pull Request Process
104 | 
105 | 1. Update documentation for any new features or changes
106 | 2. Ensure all tests pass locally
107 | 3. Update CHANGELOG.md if applicable
108 | 4. Submit PR with clear description of changes
109 | 5. Address any review feedback
110 | 6. Ensure CI checks pass
111 | 7. Squash commits if requested
112 | 
113 | ## Additional Resources
114 | 
115 | - [Architecture Documentation](ARCHITECTURE.md)
116 | - [API Documentation](docs/API.md)
117 | - [Error Handling](docs/ERRORS.md)
118 | - [Examples](docs/EXAMPLES.md)
119 | 
120 | ## Questions or Need Help?
121 | 
122 | Feel free to open an issue for:
123 | - Bug reports
124 | - Feature requests
125 | - Questions about the codebase
126 | - Suggestions for improvements
127 | 
128 | ## License
129 | 
130 | By contributing, you agree that your contributions will be licensed under the MIT License.
131 | 
```

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

```markdown
  1 | # Contributor Covenant Code of Conduct
  2 | 
  3 | ## Our Pledge
  4 | 
  5 | We as members, contributors, and leaders pledge to make participation in our
  6 | community a harassment-free experience for everyone, regardless of age, body
  7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
  8 | identity and expression, level of experience, education, socio-economic status,
  9 | nationality, personal appearance, race, religion, or sexual identity
 10 | and orientation.
 11 | 
 12 | We pledge to act and interact in ways that contribute to an open, welcoming,
 13 | diverse, inclusive, and healthy community.
 14 | 
 15 | ## Our Standards
 16 | 
 17 | Examples of behavior that contributes to a positive environment for our
 18 | community include:
 19 | 
 20 | * Demonstrating empathy and kindness toward other people
 21 | * Being respectful of differing opinions, viewpoints, and experiences
 22 | * Giving and gracefully accepting constructive feedback
 23 | * Accepting responsibility and apologizing to those affected by our mistakes,
 24 |   and learning from the experience
 25 | * Focusing on what is best not just for us as individuals, but for the
 26 |   overall community
 27 | 
 28 | Examples of unacceptable behavior include:
 29 | 
 30 | * The use of sexualized language or imagery, and sexual attention or
 31 |   advances of any kind
 32 | * Trolling, insulting or derogatory comments, and personal or political attacks
 33 | * Public or private harassment
 34 | * Publishing others' private information, such as a physical or email
 35 |   address, without their explicit permission
 36 | * Other conduct which could reasonably be considered inappropriate in a
 37 |   professional setting
 38 | 
 39 | ## Enforcement Responsibilities
 40 | 
 41 | Community leaders are responsible for clarifying and enforcing our standards of
 42 | acceptable behavior and will take appropriate and fair corrective action in
 43 | response to any behavior that they deem inappropriate, threatening, offensive,
 44 | or harmful.
 45 | 
 46 | Community leaders have the right and responsibility to remove, edit, or reject
 47 | comments, commits, code, wiki edits, issues, and other contributions that are
 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
 49 | decisions when appropriate.
 50 | 
 51 | ## Scope
 52 | 
 53 | This Code of Conduct applies within all community spaces, and also applies when
 54 | an individual is officially representing the community in public spaces.
 55 | Examples of representing our community include using an official e-mail address,
 56 | posting via an official social media account, or acting as an appointed
 57 | representative at an online or offline event.
 58 | 
 59 | ## Enforcement
 60 | 
 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
 62 | reported to the community leaders responsible for enforcement at
 63 | [email protected]. All complaints will be reviewed and investigated promptly
 64 | and fairly.
 65 | 
 66 | All community leaders are obligated to respect the privacy and security of the
 67 | reporter of any incident.
 68 | 
 69 | ## Enforcement Guidelines
 70 | 
 71 | Community leaders will follow these Community Impact Guidelines in determining
 72 | the consequences for any action they deem in violation of this Code of Conduct:
 73 | 
 74 | ### 1. Correction
 75 | 
 76 | **Community Impact**: Use of inappropriate language or other behavior deemed
 77 | unprofessional or unwelcome in the community.
 78 | 
 79 | **Consequence**: A private, written warning from community leaders, providing
 80 | clarity around the nature of the violation and an explanation of why the
 81 | behavior was inappropriate. A public apology may be requested.
 82 | 
 83 | ### 2. Warning
 84 | 
 85 | **Community Impact**: A violation through a single incident or series of
 86 | actions.
 87 | 
 88 | **Consequence**: A warning with consequences for continued behavior. No
 89 | interaction with the people involved, including unsolicited interaction with
 90 | those enforcing the Code of Conduct, for a specified period of time. This
 91 | includes avoiding interactions in community spaces as well as external channels
 92 | like social media. Violating these terms may lead to a temporary or permanent
 93 | ban.
 94 | 
 95 | ### 3. Temporary Ban
 96 | 
 97 | **Community Impact**: A serious violation of community standards, including
 98 | sustained inappropriate behavior.
 99 | 
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 | 
106 | ### 4. Permanent Ban
107 | 
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression towar
111 | 
```

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

```yaml
1 | github: [aaronsb]
2 | 
```

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

```json
1 | {
2 |   "type": "commonjs"
3 | }
4 | 
```

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

```
1 | // Jest setup file
2 | require('@jest/globals');
3 | 
```

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

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

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

```yaml
1 | 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
 1 | export class StdioServerTransport {
 2 |   constructor() {}
 3 | 
 4 |   async connect() {
 5 |     return Promise.resolve();
 6 |   }
 7 | 
 8 |   async disconnect() {
 9 |     return Promise.resolve();
10 |   }
11 | }
12 | 
```

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

```typescript
1 | import { DriveService } from '../../modules/drive/service.js';
2 | 
3 | // Create singleton instance
4 | const driveService = new DriveService();
5 | 
6 | // Export singleton instance
7 | export { driveService };
8 | 
```

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

```javascript
 1 | export class Server {
 2 |   constructor(config, options) {
 3 |     this.config = config;
 4 |     this.options = options;
 5 |   }
 6 | 
 7 |   async connect() {
 8 |     return Promise.resolve();
 9 |   }
10 | 
11 |   setRequestHandler() {
12 |     // Mock implementation
13 |   }
14 | }
15 | 
```

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

```typescript
 1 | /**
 2 |  * Validate email address format
 3 |  */
 4 | export function validateEmail(email: string): void {
 5 |   const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
 6 |   if (!emailRegex.test(email)) {
 7 |     throw new Error(`Invalid email address: ${email}`);
 8 |   }
 9 | }
10 | 
```

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

```json
1 | {
2 |   "client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",
3 |   "client_secret": "YOUR_CLIENT_SECRET",
4 |   "redirect_uri": "http://localhost:8080",
5 |   "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth",
6 |   "token_uri": "https://oauth2.googleapis.com/token"
7 | }
8 | 
```

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

```typescript
 1 | declare module '@modelcontextprotocol/sdk' {
 2 |   export class McpError extends Error {
 3 |     constructor(code: ErrorCode, message: string, details?: Record<string, any>);
 4 |   }
 5 | 
 6 |   export enum ErrorCode {
 7 |     InternalError = 'INTERNAL_ERROR',
 8 |     InvalidRequest = 'INVALID_REQUEST',
 9 |     MethodNotFound = 'METHOD_NOT_FOUND',
10 |     InvalidParams = 'INVALID_PARAMS'
11 |   }
12 | }
13 | 
```

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

```typescript
 1 | #!/usr/bin/env node
 2 | import { GSuiteServer } from './tools/server.js';
 3 | 
 4 | // Start server with proper shutdown handling
 5 | const server = new GSuiteServer();
 6 | 
 7 | // Handle process signals
 8 | process.on('SIGINT', () => {
 9 |   process.exit(0);
10 | });
11 | 
12 | process.on('SIGTERM', () => {
13 |   process.exit(0);
14 | });
15 | 
16 | // Start with error handling
17 | server.run().catch(error => {
18 |   console.error('Fatal Error:', error);
19 |   process.exit(1);
20 | });
21 | 
```

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

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "node16",
 5 |     "moduleResolution": "node16",
 6 |     "esModuleInterop": true,
 7 |     "forceConsistentCasingInFileNames": true,
 8 |     "strict": true,
 9 |     "skipLibCheck": true,
10 |     "outDir": "build",
11 |     "rootDir": "src",
12 |     "declaration": true,
13 |     "sourceMap": true,
14 |     "typeRoots": ["./node_modules/@types", "./src/types"]
15 |   },
16 |   "include": ["src/**/*"],
17 |   "exclude": ["node_modules", "build"]
18 | }
19 | 
```

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

```typescript
 1 | import { scopeRegistry } from "../../modules/tools/scope-registry.js";
 2 | 
 3 | // Define Contacts scopes as constants
 4 | // Reference: https://developers.google.com/people/api/rest/v1/people.connections/list (and other People API docs)
 5 | export const CONTACTS_SCOPES = {
 6 |   READONLY: "https://www.googleapis.com/auth/contacts.readonly",
 7 |   // Add other scopes like write/modify later if needed
 8 |   // CONTACTS: 'https://www.googleapis.com/auth/contacts'
 9 | };
10 | 
11 | // Register the contacts scopes with the scope registry
12 | scopeRegistry.registerScope("contacts", CONTACTS_SCOPES.READONLY);
13 | 
```

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

```typescript
 1 | /**
 2 |  * Contacts module entry point.
 3 |  * Exports types, services, and initialization functions for the contacts module.
 4 |  */
 5 | 
 6 | // Export types
 7 | export * from './types.js';
 8 | 
 9 | // Export scopes
10 | export { CONTACTS_SCOPES } from './scopes.js';
11 | 
12 | /**
13 |  * Initialize the contacts module.
14 |  * This function is called during server startup to set up any required resources.
15 |  */
16 | export async function initializeContactsModule(): Promise<void> {
17 |   // Currently no async initialization needed
18 |   // This function is a placeholder for future initialization logic
19 |   return Promise.resolve();
20 | }
21 | 
```

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

```typescript
 1 | #!/usr/bin/env node
 2 | 
 3 | import { GSuiteServer } from '../tools/server.js';
 4 | import logger from '../utils/logger.js';
 5 | 
 6 | async function checkHealth() {
 7 |   try {
 8 |     const server = new GSuiteServer();
 9 |     
10 |     // Attempt to start server
11 |     await server.run();
12 |     
13 |     // If we get here without errors, consider it healthy
14 |     logger.info('Health check passed');
15 |     process.exit(0);
16 |   } catch (error) {
17 |     logger.error('Health check failed:', error);
18 |     process.exit(1);
19 |   }
20 | }
21 | 
22 | checkHealth().catch(error => {
23 |   logger.error('Fatal error during health check:', error);
24 |   process.exit(1);
25 | });
26 | 
```

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

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - configDir
10 |     properties:
11 |       configDir:
12 |         type: string
13 |         description: The directory path where 'gauth.json' and 'accounts.json' are located.
14 |   commandFunction:
15 |     # A function that produces the CLI command to start the MCP on stdio.
16 |     |-
17 |     (config) => ({ command: 'docker', args: ['run', '-v', `${config.configDir}:/app/config`, 'gsuite-mcp'] })
18 | 
```

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

```typescript
 1 | import { GmailService } from './services/base.js';
 2 | import { GmailError } from './types.js';
 3 | import { DraftService } from './services/draft.js';
 4 | 
 5 | export { GmailService, GmailError, DraftService };
 6 | 
 7 | let gmailService: GmailService | null = null;
 8 | 
 9 | export async function initializeGmailModule(): Promise<void> {
10 |   gmailService = new GmailService();
11 |   await gmailService.initialize();
12 | }
13 | 
14 | export function getGmailService(): GmailService {
15 |   if (!gmailService) {
16 |     throw new GmailError(
17 |       'Gmail module not initialized',
18 |       'MODULE_NOT_INITIALIZED',
19 |       'Call initializeGmailModule before using the Gmail service'
20 |     );
21 |   }
22 |   return gmailService;
23 | }
24 | 
```

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

```json
 1 | {
 2 |   "accounts": [
 3 |     {
 4 |       "email": "[email protected]",
 5 |       "category": "personal",
 6 |       "description": "Personal Google Account",
 7 |       "auth_status": {
 8 |         "valid": true,
 9 |         "token": {
10 |           "access_token": "YOUR_ACCESS_TOKEN",
11 |           "refresh_token": "YOUR_REFRESH_TOKEN",
12 |           "scope": "https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/gmail.send",
13 |           "token_type": "Bearer",
14 |           "expiry_date": 1737951212015,
15 |           "last_refresh": 1737947613015
16 |         }
17 |       }
18 |     },
19 |     {
20 |       "email": "[email protected]",
21 |       "category": "work",
22 |       "description": "Work Google Account"
23 |     }
24 |   ]
25 | }
26 | 
```

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

```typescript
 1 | export class McpError extends Error {
 2 |   code: string;
 3 |   details?: Record<string, any>;
 4 | 
 5 |   constructor(code: string, message: string, details?: Record<string, any>) {
 6 |     super(message);
 7 |     this.code = code;
 8 |     this.details = details;
 9 |     this.name = 'McpError';
10 |   }
11 | }
12 | 
13 | export enum ErrorCode {
14 |   InternalError = 'INTERNAL_ERROR',
15 |   InvalidRequest = 'INVALID_REQUEST',
16 |   MethodNotFound = 'METHOD_NOT_FOUND',
17 |   InvalidParams = 'INVALID_PARAMS',
18 |   AuthenticationRequired = 'AUTHENTICATION_REQUIRED',
19 |   AuthenticationFailed = 'AUTHENTICATION_FAILED',
20 |   PermissionDenied = 'PERMISSION_DENIED',
21 |   ResourceNotFound = 'RESOURCE_NOT_FOUND',
22 |   ServiceUnavailable = 'SERVICE_UNAVAILABLE',
23 |   ParseError = 'PARSE_ERROR'
24 | }
25 | 
```

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

```typescript
 1 | import { jest } from '@jest/globals';
 2 | 
 3 | export const google = {
 4 |   drive: jest.fn().mockReturnValue({
 5 |     files: {
 6 |       list: jest.fn(),
 7 |       create: jest.fn(),
 8 |       get: jest.fn(),
 9 |       delete: jest.fn(),
10 |       export: jest.fn()
11 |     },
12 |     permissions: {
13 |       create: jest.fn()
14 |     }
15 |   }),
16 |   gmail: jest.fn().mockReturnValue({
17 |     users: {
18 |       messages: {
19 |         list: jest.fn(),
20 |         get: jest.fn(),
21 |         send: jest.fn()
22 |       },
23 |       drafts: {
24 |         create: jest.fn(),
25 |         list: jest.fn(),
26 |         get: jest.fn(),
27 |         send: jest.fn()
28 |       },
29 |       getProfile: jest.fn(),
30 |       settings: {
31 |         getAutoForwarding: jest.fn(),
32 |         getImap: jest.fn(),
33 |         getLanguage: jest.fn(),
34 |         getPop: jest.fn(),
35 |         getVacation: jest.fn()
36 |       }
37 |     }
38 |   })
39 | };
40 | 
```

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

```markdown
 1 | ---
 2 | name: Bug Report
 3 | about: Create a report to help us improve
 4 | title: '[BUG] '
 5 | labels: bug
 6 | assignees: ''
 7 | ---
 8 | 
 9 | ## Bug Description
10 | A clear and concise description of what the bug is.
11 | 
12 | ## Steps To Reproduce
13 | 1. Go to '...'
14 | 2. Click on '....'
15 | 3. Scroll down to '....'
16 | 4. See error
17 | 
18 | ## Expected Behavior
19 | A clear and concise description of what you expected to happen.
20 | 
21 | ## Actual Behavior
22 | A clear and concise description of what actually happened.
23 | 
24 | ## Screenshots
25 | If applicable, add screenshots to help explain your problem.
26 | 
27 | ## Environment
28 | - OS: [e.g. Linux, macOS, Windows]
29 | - Node.js Version: [e.g. 18.15.0]
30 | - NPM Version: [e.g. 9.5.0]
31 | - Google Workspace MCP Version: [e.g. 1.0.0]
32 | 
33 | ## Additional Context
34 | Add any other context about the problem here, such as:
35 | - Related issues
36 | - Error messages/logs
37 | - Configuration details
38 | 
```

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

```markdown
 1 | # Product Context
 2 | 
 3 | ## Purpose
 4 | This project implements a Model Context Protocol (MCP) server that provides authenticated access to Google Workspace APIs, specifically focusing on Gmail and Calendar functionality.
 5 | 
 6 | ## Problems Solved
 7 | 1. Provides a standardized interface for AI systems to interact with Google Workspace services
 8 | 2. Handles complex OAuth authentication flows and token management
 9 | 3. Manages multiple Google accounts securely
10 | 4. Simplifies integration with Gmail and Calendar services
11 | 
12 | ## How It Works
13 | - Implements a modular architecture focused on Gmail and Calendar functionality
14 | - Uses OAuth 2.0 for authentication with automatic token refresh
15 | - Provides simple verb-noun interfaces for AI agents
16 | - Follows "simplest viable design" principle to prevent over-engineering
17 | - Handles authentication through HTTP response codes (401/403)
18 | - Moves OAuth mechanics into platform infrastructure
19 | 
```

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

```typescript
 1 | import { driveService } from '../../services/drive/index.js';
 2 | import { DriveService } from './service.js';
 3 | import { DriveOperationResult } from './types.js';
 4 | 
 5 | // Export types and service
 6 | export * from './types.js';
 7 | export * from './scopes.js';
 8 | export { DriveService };
 9 | 
10 | // Get singleton instance
11 | let serviceInstance: DriveService | undefined;
12 | 
13 | export async function getDriveService(): Promise<DriveService> {
14 |   if (!serviceInstance) {
15 |     serviceInstance = driveService;
16 |     await serviceInstance.ensureInitialized();
17 |   }
18 |   return serviceInstance;
19 | }
20 | 
21 | // Initialize module
22 | export async function initializeDriveModule(): Promise<void> {
23 |   const service = await getDriveService();
24 |   await service.ensureInitialized();
25 | }
26 | 
27 | // Helper to handle errors consistently
28 | export function handleDriveError(error: unknown): DriveOperationResult {
29 |   return {
30 |     success: false,
31 |     error: error instanceof Error ? error.message : 'Unknown error occurred',
32 |   };
33 | }
34 | 
```

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

```typescript
 1 | import { AccountManager } from './manager.js';
 2 | import { TokenManager } from './token.js';
 3 | import { GoogleOAuthClient } from './oauth.js';
 4 | import { Account, AccountError, TokenStatus, AccountModuleConfig } from './types.js';
 5 | 
 6 | // Create singleton instance
 7 | let accountManager: AccountManager | null = null;
 8 | 
 9 | export async function initializeAccountModule(config?: AccountModuleConfig): Promise<AccountManager> {
10 |   if (!accountManager) {
11 |     accountManager = new AccountManager(config);
12 |     await accountManager.initialize();
13 |   }
14 |   return accountManager;
15 | }
16 | 
17 | export function getAccountManager(): AccountManager {
18 |   if (!accountManager) {
19 |     throw new AccountError(
20 |       'Account module not initialized',
21 |       'MODULE_NOT_INITIALIZED',
22 |       'Call initializeAccountModule before using the account manager'
23 |     );
24 |   }
25 |   return accountManager;
26 | }
27 | 
28 | export {
29 |   AccountManager,
30 |   TokenManager,
31 |   GoogleOAuthClient,
32 |   Account,
33 |   AccountError,
34 |   TokenStatus,
35 |   AccountModuleConfig
36 | };
37 | 
```

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

```typescript
 1 | import { drive_v3 } from 'googleapis';
 2 | 
 3 | export type DriveFile = drive_v3.Schema$File;
 4 | export type DriveFileList = drive_v3.Schema$FileList;
 5 | export type DrivePermission = drive_v3.Schema$Permission;
 6 | 
 7 | export interface FileUploadOptions {
 8 |   name: string;
 9 |   mimeType?: string;
10 |   parents?: string[];
11 |   content: string | Buffer;
12 | }
13 | 
14 | export interface FileDownloadOptions {
15 |   fileId: string;
16 |   mimeType?: string;
17 | }
18 | 
19 | export interface FileListOptions {
20 |   folderId?: string;
21 |   query?: string;
22 |   pageSize?: number;
23 |   orderBy?: string[];
24 |   fields?: string[];
25 | }
26 | 
27 | export interface FileSearchOptions extends FileListOptions {
28 |   fullText?: string;
29 |   mimeType?: string;
30 |   trashed?: boolean;
31 | }
32 | 
33 | export interface PermissionOptions {
34 |   fileId: string;
35 |   role: 'owner' | 'organizer' | 'fileOrganizer' | 'writer' | 'commenter' | 'reader';
36 |   type: 'user' | 'group' | 'domain' | 'anyone';
37 |   emailAddress?: string;
38 |   domain?: string;
39 |   allowFileDiscovery?: boolean;
40 | }
41 | 
42 | export interface DriveOperationResult {
43 |   success: boolean;
44 |   data?: any;
45 |   error?: string;
46 |   mimeType?: string;
47 |   filePath?: string;
48 | }
49 | 
```

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

```typescript
 1 | // Mock logger that respects LOG_MODE setting
 2 | const LOG_MODE = (process.env.LOG_MODE || 'normal') as 'normal' | 'strict';
 3 | 
 4 | const isJsonRpc = (msg: any): boolean => {
 5 |   if (typeof msg !== 'string') return false;
 6 |   return msg.startsWith('{"jsonrpc":') || msg.startsWith('{"id":');
 7 | };
 8 | 
 9 | export default {
10 |   error: (...args: any[]) => process.stderr.write(args.join(' ') + '\n'),
11 |   warn: (...args: any[]) => {
12 |     if (LOG_MODE === 'strict' || isJsonRpc(args[0])) {
13 |       process.stderr.write(args.join(' ') + '\n');
14 |     } else {
15 |       process.stdout.write('[WARN] ' + args.join(' ') + '\n');
16 |     }
17 |   },
18 |   info: (...args: any[]) => {
19 |     if (LOG_MODE === 'strict' || isJsonRpc(args[0])) {
20 |       process.stderr.write(args.join(' ') + '\n');
21 |     } else {
22 |       process.stdout.write('[INFO] ' + args.join(' ') + '\n');
23 |     }
24 |   },
25 |   debug: (...args: any[]) => {
26 |     if (!process.env.DEBUG) return;
27 |     if (LOG_MODE === 'strict' || isJsonRpc(args[0])) {
28 |       process.stderr.write(args.join(' ') + '\n');
29 |     } else {
30 |       process.stdout.write('[DEBUG] ' + args.join(' ') + '\n');
31 |     }
32 |   }
33 | };
34 | 
```

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

```markdown
 1 | ---
 2 | name: Feature Request
 3 | about: Suggest an idea for this project
 4 | title: '[FEATURE] '
 5 | labels: enhancement
 6 | assignees: ''
 7 | ---
 8 | 
 9 | ## Problem Statement
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 | 
12 | ## Proposed Solution
13 | A clear and concise description of what you want to happen.
14 | 
15 | ## Alternative Solutions
16 | A clear and concise description of any alternative solutions or features you've considered.
17 | 
18 | ## Implementation Details
19 | If you have specific ideas about how to implement this feature, please share them here.
20 | Consider:
21 | - API changes needed
22 | - New configuration options
23 | - Impact on existing functionality
24 | - Security considerations
25 | 
26 | ## Benefits
27 | Describe the benefits this feature would bring to:
28 | - Users of the library
29 | - Developers maintaining the code
30 | - The project's overall goals
31 | 
32 | ## Additional Context
33 | Add any other context, screenshots, or examples about the feature request here.
34 | 
35 | ## Checklist
36 | - [ ] I have searched for similar feature requests
37 | - [ ] This feature aligns with the project's scope
38 | - [ ] I'm willing to help implement this feature
39 | 
```

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

```typescript
 1 | import { AttachmentIndexService } from './index-service.js';
 2 | 
 3 | /**
 4 |  * Simplified attachment information visible to AI
 5 |  */
 6 | export interface AttachmentInfo {
 7 |   name: string;
 8 | }
 9 | 
10 | /**
11 |  * Transform attachment data for AI consumption while storing full metadata
12 |  */
13 | export class AttachmentTransformer {
14 |   constructor(private indexService: AttachmentIndexService) {}
15 | 
16 |   /**
17 |    * Transform attachments for a message, storing metadata and returning simplified format
18 |    */
19 |   transformAttachments(messageId: string, attachments: Array<{
20 |     id: string;
21 |     name: string;
22 |     mimeType: string;
23 |     size: number;
24 |   }>): AttachmentInfo[] {
25 |     // Store full metadata for each attachment
26 |     attachments.forEach(attachment => {
27 |       this.indexService.addAttachment(messageId, attachment);
28 |     });
29 | 
30 |     // Return simplified format for AI
31 |     return attachments.map(attachment => ({
32 |       name: attachment.name
33 |     }));
34 |   }
35 | 
36 |   /**
37 |    * Create a refresh placeholder when attachments have expired
38 |    */
39 |   static createRefreshPlaceholder(): AttachmentInfo[] {
40 |     return [{
41 |       name: "Attachments expired - Request message again to view"
42 |     }];
43 |   }
44 | }
45 | 
```

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

```
 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
 2 | module.exports = {
 3 |   preset: 'ts-jest/presets/default-esm',
 4 |   testEnvironment: 'node',
 5 |   extensionsToTreatAsEsm: ['.ts'],
 6 |   moduleNameMapper: {
 7 |     '^(\\.{1,2}/.*)\\.js$': '$1',
 8 |     '^@modelcontextprotocol/sdk/(.*)$': '<rootDir>/src/__mocks__/@modelcontextprotocol/sdk.ts',
 9 |     '^@modelcontextprotocol/sdk$': '<rootDir>/src/__mocks__/@modelcontextprotocol/sdk.ts',
10 |     '^src/utils/logger.js$': '<rootDir>/src/__mocks__/logger.ts',
11 |     '^../utils/logger.js$': '<rootDir>/src/__mocks__/logger.ts',
12 |     '^../../utils/logger.js$': '<rootDir>/src/__mocks__/logger.ts'
13 |   },
14 |   transform: {
15 |     '^.+\\.tsx?$': [
16 |       'ts-jest',
17 |       {
18 |         useESM: true,
19 |         tsconfig: 'tsconfig.json'
20 |       },
21 |     ],
22 |   },
23 |   moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
24 |   testMatch: ['**/__tests__/**/*.test.ts'],
25 |   transformIgnorePatterns: [
26 |     'node_modules/(?!(googleapis|google-auth-library|@modelcontextprotocol/sdk|zod)/)'
27 |   ],
28 |   setupFilesAfterEnv: ['<rootDir>/src/__helpers__/testSetup.ts'],
29 |   fakeTimers: {
30 |     enableGlobally: true,
31 |     now: new Date('2025-02-21T20:30:00Z').getTime()
32 |   }
33 | };
34 | 
```

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

```typescript
 1 | export class McpError extends Error {
 2 |   code: string;
 3 |   details?: Record<string, any>;
 4 | 
 5 |   constructor(code: string, message: string, details?: Record<string, any>) {
 6 |     super(message);
 7 |     this.code = code;
 8 |     this.details = details;
 9 |     this.name = 'McpError';
10 |   }
11 | }
12 | 
13 | export enum ErrorCode {
14 |   InternalError = 'INTERNAL_ERROR',
15 |   InvalidRequest = 'INVALID_REQUEST',
16 |   MethodNotFound = 'METHOD_NOT_FOUND',
17 |   InvalidParams = 'INVALID_PARAMS',
18 |   AuthenticationRequired = 'AUTHENTICATION_REQUIRED',
19 |   AuthenticationFailed = 'AUTHENTICATION_FAILED',
20 |   PermissionDenied = 'PERMISSION_DENIED',
21 |   ResourceNotFound = 'RESOURCE_NOT_FOUND',
22 |   ServiceUnavailable = 'SERVICE_UNAVAILABLE',
23 |   ParseError = 'PARSE_ERROR'
24 | }
25 | 
26 | export class Server {
27 |   private config: any;
28 |   public onerror: ((error: Error) => void) | undefined;
29 | 
30 |   constructor(config: any, options?: any) {
31 |     this.config = config;
32 |   }
33 | 
34 |   async connect(transport: any): Promise<void> {
35 |     // Mock implementation
36 |     return Promise.resolve();
37 |   }
38 | 
39 |   async close(): Promise<void> {
40 |     // Mock implementation
41 |     return Promise.resolve();
42 |   }
43 | 
44 |   setRequestHandler(schema: any, handler: any): void {
45 |     // Mock implementation
46 |   }
47 | }
48 | 
```

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

```typescript
 1 | export const mockAccounts = {
 2 |   accounts: [
 3 |     {
 4 |       email: '[email protected]',
 5 |       category: 'work',
 6 |       description: 'Test Work Account'
 7 |     },
 8 |     {
 9 |       email: '[email protected]',
10 |       category: 'personal',
11 |       description: 'Test Personal Account'
12 |     }
13 |   ]
14 | };
15 | 
16 | export const mockTokens = {
17 |   valid: {
18 |     access_token: 'valid-token',
19 |     refresh_token: 'refresh-token',
20 |     expiry_date: Date.now() + 3600000
21 |   },
22 |   expired: {
23 |     access_token: 'expired-token',
24 |     refresh_token: 'refresh-token',
25 |     expiry_date: Date.now() - 3600000
26 |   }
27 | };
28 | 
29 | export const mockGmailResponses = {
30 |   messageList: {
31 |     messages: [
32 |       { id: 'msg1', threadId: 'thread1' },
33 |       { id: 'msg2', threadId: 'thread2' }
34 |     ],
35 |     resultSizeEstimate: 2
36 |   },
37 |   message: {
38 |     id: 'msg1',
39 |     threadId: 'thread1',
40 |     labelIds: ['INBOX'],
41 |     snippet: 'Email snippet',
42 |     payload: {
43 |       headers: [
44 |         { name: 'From', value: '[email protected]' },
45 |         { name: 'Subject', value: 'Test Subject' },
46 |         { name: 'To', value: '[email protected]' },
47 |         { name: 'Date', value: '2024-01-01T00:00:00Z' }
48 |       ],
49 |       parts: [
50 |         {
51 |           mimeType: 'text/plain',
52 |           body: { data: Buffer.from('Test content').toString('base64') }
53 |         }
54 |       ]
55 |     }
56 |   }
57 | };
58 | 
```

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

```markdown
 1 | # Technical Context
 2 | 
 3 | ## Technologies Used
 4 | - TypeScript/Node.js for server implementation
 5 | - Google Workspace APIs (Gmail, Calendar)
 6 | - OAuth 2.0 for authentication
 7 | - Model Context Protocol (MCP) for AI integration
 8 | 
 9 | ## Development Setup
10 | 1. **Required Configuration Files**
11 |    - `config/gauth.json`: OAuth credentials
12 |    - `config/accounts.json`: Account configurations
13 |    - `config/credentials/`: Token storage
14 | 
15 | 2. **Environment Variables**
16 |    - AUTH_CONFIG_FILE: OAuth credentials path
17 |    - ACCOUNTS_FILE: Account config path
18 |    - CREDENTIALS_DIR: Token storage path
19 | 
20 | ## Technical Constraints
21 | 1. **OAuth & Authentication**
22 |    - Must handle token refresh flows
23 |    - Requires proper scope management
24 |    - Needs secure token storage
25 | 
26 | 2. **API Limitations**
27 |    - Gmail API rate limits
28 |    - Calendar API quotas
29 |    - OAuth token expiration
30 | 
31 | 3. **Tool Registration**
32 |    - Tools must be registered in both ListToolsRequestSchema and CallToolRequestSchema
33 |    - Must follow verb-noun naming convention
34 | 
35 | 4. **Error Handling**
36 |    - Must handle auth errors (401/403)
37 |    - Must implement automatic token refresh
38 |    - Must provide clear error messages
39 | 
40 | 5. **Security Requirements**
41 |    - Secure credential storage
42 |    - Token encryption
43 |    - Environment-based configuration
44 |    - No sensitive data in version control
45 | 
```

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

```typescript
 1 | import { OAuth2Client } from 'google-auth-library';
 2 | 
 3 | export interface Account {
 4 |   email: string;
 5 |   category: string;
 6 |   description: string;
 7 |   auth_status?: {
 8 |     valid: boolean;
 9 |     status?: TokenStatusType;
10 |     token?: any;  // Internal use only - not exposed to AI
11 |     reason?: string;
12 |     authUrl?: string;
13 |     requiredScopes?: string[];
14 |   };
15 | }
16 | 
17 | export interface AccountsConfig {
18 |   accounts: Account[];
19 | }
20 | 
21 | export type TokenStatusType = 
22 |   | 'NO_TOKEN'
23 |   | 'VALID'
24 |   | 'INVALID'
25 |   | 'REFRESHED'
26 |   | 'REFRESH_FAILED'
27 |   | 'EXPIRED'
28 |   | 'ERROR';
29 | 
30 | export interface TokenRenewalResult {
31 |   success: boolean;
32 |   status: TokenStatusType;
33 |   reason?: string;
34 |   token?: any;
35 |   canRetry?: boolean;  // Indicates if a failed refresh can be retried later
36 | }
37 | 
38 | export interface TokenStatus {
39 |   valid: boolean;
40 |   status: TokenStatusType;
41 |   token?: any;
42 |   reason?: string;
43 |   authUrl?: string;
44 |   requiredScopes?: string[];
45 | }
46 | 
47 | export interface AuthenticationError extends AccountError {
48 |   authUrl: string;
49 |   requiredScopes: string[];
50 | }
51 | 
52 | export interface AccountModuleConfig {
53 |   accountsPath?: string;
54 |   oauth2Client?: OAuth2Client;
55 | }
56 | 
57 | export class AccountError extends Error {
58 |   constructor(
59 |     message: string,
60 |     public code: string,
61 |     public resolution: string
62 |   ) {
63 |     super(message);
64 |     this.name = 'AccountError';
65 |   }
66 | }
67 | 
```

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

```typescript
 1 | export interface AttachmentMetadata {
 2 |   id: string;           // Unique identifier
 3 |   name: string;         // Original filename
 4 |   mimeType: string;     // MIME type
 5 |   size: number;         // File size in bytes
 6 |   path: string;         // Local filesystem path
 7 | }
 8 | 
 9 | export interface AttachmentSource {
10 |   content: string;      // Base64 content
11 |   metadata: {
12 |     name: string;
13 |     mimeType: string;
14 |     size?: number;
15 |   };
16 | }
17 | 
18 | export interface AttachmentResult {
19 |   success: boolean;
20 |   attachment?: AttachmentMetadata;
21 |   error?: string;
22 | }
23 | 
24 | export interface AttachmentServiceConfig {
25 |   maxSizeBytes?: number;                    // Maximum file size (default: 25MB)
26 |   allowedMimeTypes?: string[];             // Allowed MIME types (default: all)
27 |   basePath?: string;                       // Base path for attachments (default: WORKSPACE_BASE_PATH/attachments)
28 |   quotaLimitBytes?: number;                // Storage quota limit
29 | }
30 | 
31 | export interface AttachmentValidationResult {
32 |   valid: boolean;
33 |   error?: string;
34 | }
35 | 
36 | // Folder structure constants
37 | export type AttachmentFolderType = 'attachments' | 'email' | 'calendar' | 'incoming' | 'outgoing' | 'event-files';
38 | 
39 | export const ATTACHMENT_FOLDERS: Record<string, AttachmentFolderType> = {
40 |   ROOT: 'attachments',
41 |   EMAIL: 'email',
42 |   CALENDAR: 'calendar',
43 |   INCOMING: 'incoming',
44 |   OUTGOING: 'outgoing',
45 |   EVENT_FILES: 'event-files'
46 | };
47 | 
```

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

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Load environment variables from .env file
 4 | if [ -f .env ]; then
 5 |     export $(cat .env | grep -v '^#' | xargs)
 6 | fi
 7 | 
 8 | # Function to log error messages
 9 | log_error() {
10 |     echo "[ERROR] $1" >&2
11 | }
12 | 
13 | # Function to log info messages
14 | log_info() {
15 |     echo "[INFO] $1" >&2
16 | }
17 | 
18 | # Validate required environment variables
19 | if [ -z "$GOOGLE_CLIENT_ID" ]; then
20 |     log_error "GOOGLE_CLIENT_ID environment variable is required"
21 |     exit 1
22 | fi
23 | 
24 | if [ -z "$GOOGLE_CLIENT_SECRET" ]; then
25 |     log_error "GOOGLE_CLIENT_SECRET environment variable is required"
26 |     exit 1
27 | fi
28 | 
29 | # Set default workspace path if not provided
30 | if [ -z "$WORKSPACE_BASE_PATH" ]; then
31 |     export WORKSPACE_BASE_PATH="$HOME/Documents/workspace-mcp-files"
32 | fi
33 | 
34 | # Create necessary directories
35 | mkdir -p "$HOME/.mcp/google-workspace-mcp"
36 | mkdir -p "$WORKSPACE_BASE_PATH"
37 | 
38 | # Link config to expected location (symlink approach)
39 | mkdir -p /tmp/app/config
40 | ln -sf "$HOME/.mcp/google-workspace-mcp/accounts.json" /tmp/app/config/accounts.json 2>/dev/null || true
41 | 
42 | # If accounts.json doesn't exist, create it
43 | if [ ! -f "$HOME/.mcp/google-workspace-mcp/accounts.json" ]; then
44 |     log_info "Creating accounts.json"
45 |     echo '{}' > "$HOME/.mcp/google-workspace-mcp/accounts.json"
46 | fi
47 | 
48 | # Trap signals for clean shutdown
49 | trap 'log_info "Shutting down..."; exit 0' SIGTERM SIGINT
50 | 
51 | # Execute the main application
52 | exec node build/index.js "$@"
```

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

```dockerfile
 1 | # syntax=docker/dockerfile:1.4
 2 | 
 3 | # Build stage
 4 | FROM --platform=$BUILDPLATFORM node:20-slim AS builder
 5 | WORKDIR /app
 6 | 
 7 | # Add metadata
 8 | LABEL org.opencontainers.image.source="https://github.com/aaronsb/google-workspace-mcp"
 9 | LABEL org.opencontainers.image.description="Google Workspace MCP Server"
10 | LABEL org.opencontainers.image.licenses="MIT"
11 | 
12 | # Install dependencies first (better layer caching)
13 | COPY package*.json ./
14 | RUN --mount=type=cache,target=/root/.npm,sharing=locked \
15 |     npm ci --prefer-offline --no-audit --no-fund
16 | 
17 | # Copy source and build
18 | COPY . .
19 | RUN --mount=type=cache,target=/root/.npm,sharing=locked \
20 |     npm run build
21 | 
22 | # Production stage
23 | FROM node:20-slim AS production
24 | WORKDIR /app
25 | 
26 | # Set docker hash as environment variable
27 | ARG DOCKER_HASH=unknown
28 | ENV DOCKER_HASH=$DOCKER_HASH
29 | 
30 | # Copy only necessary files from builder
31 | COPY --from=builder /app/build ./build
32 | COPY --from=builder /app/package*.json ./
33 | COPY --from=builder /app/docker-entrypoint.sh ./
34 | 
35 | # Install production dependencies and set up directories
36 | RUN --mount=type=cache,target=/root/.npm,sharing=locked \
37 |     npm ci --prefer-offline --no-audit --no-fund --omit=dev && \
38 |     npm install [email protected] && \
39 |     chmod +x build/index.js && \
40 |     chmod +x docker-entrypoint.sh && \
41 |     mkdir -p /app/logs && \
42 |     chown -R 1000:1000 /app
43 | 
44 | # Switch to non-root user
45 | USER 1000:1000
46 | 
47 | ENTRYPOINT ["./docker-entrypoint.sh"]
48 | 
```

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

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Function to log error messages
 4 | log_error() {
 5 |     echo "[ERROR] $1" >&2
 6 | }
 7 | 
 8 | # Function to log info messages
 9 | log_info() {
10 |     echo "[INFO] $1" >&2
11 | }
12 | 
13 | # Validate required environment variables
14 | if [ -z "$GOOGLE_CLIENT_ID" ]; then
15 |     log_error "GOOGLE_CLIENT_ID environment variable is required"
16 |     exit 1
17 | fi
18 | 
19 | if [ -z "$GOOGLE_CLIENT_SECRET" ]; then
20 |     log_error "GOOGLE_CLIENT_SECRET environment variable is required"
21 |     exit 1
22 | fi
23 | 
24 | # Set default workspace path if not provided
25 | if [ -z "$WORKSPACE_BASE_PATH" ]; then
26 |     export WORKSPACE_BASE_PATH="/app/workspace"
27 | fi
28 | 
29 | # Trap signals for clean shutdown
30 | trap 'log_info "Shutting down..."; exit 0' SIGTERM SIGINT
31 | 
32 | # Set environment variables
33 | export MCP_MODE=true
34 | export LOG_FILE="/app/logs/google-workspace-mcp.log"
35 | export WORKSPACE_BASE_PATH="$WORKSPACE_BASE_PATH"
36 | 
37 | # Ensure /app/config/accounts.json exists, copy from example if missing, or create minimal if both missing
38 | if [ ! -f "/app/config/accounts.json" ]; then
39 |     if [ -f "/app/config/accounts.example.json" ]; then
40 |         log_info "accounts.json not found, copying from accounts.example.json"
41 |         cp /app/config/accounts.example.json /app/config/accounts.json
42 |     else
43 |         log_info "accounts.json and accounts.example.json not found, creating minimal accounts.json"
44 |         echo '{ "accounts": [] }' > /app/config/accounts.json
45 |     fi
46 | fi
47 | 
48 | # Execute the main application
49 | exec node build/index.js
50 | 
```

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

```markdown
 1 | # Progress Status
 2 | 
 3 | ## What Works
 4 | - Account Manager Tests
 5 |   - loadAccounts functionality with Map structure
 6 |   - validateAccount initialization
 7 |   - AccountError types implementation
 8 |   - fs mocks for mkdir and dirname
 9 | - Calendar Service Tests
10 |   - ISO date format handling
11 |   - createEvent validation
12 |   - Optional parameters testing
13 |   - Invalid date handling
14 | - Gmail Service Tests
15 |   - Basic functionality verified
16 | - Error Handling
17 |   - OAuth client error handling
18 |   - Debug logging for auth config
19 |   - Error message validation
20 | 
21 | ## What's Left to Build/Investigate
22 | 1. Additional Test Coverage
23 |    - Edge cases and error scenarios
24 |    - Token refresh flows
25 |    - Rate limiting behavior
26 |    - Invalid input handling
27 |    - Concurrent operations
28 |    - MCP server operations
29 | 
30 | 2. Test Infrastructure
31 |    - Mock implementations review
32 |    - Test organization optimization
33 |    - Documentation coverage
34 |    - Error message clarity
35 | 
36 | 3. Integration Testing
37 |    - Tool registration verification
38 |    - Request validation flows
39 |    - Error propagation paths
40 |    - Authentication scenarios
41 |    - Response format validation
42 | 
43 | ## Progress Status
44 | - Account Manager Tests: ✅ Complete (14 tests)
45 | - Calendar Service Tests: ✅ Complete (11 tests)
46 | - Gmail Service Tests: ✅ Complete (6 tests)
47 | - Basic Error Handling: ✅ Complete
48 | - Edge Cases: 🔄 In Progress
49 | - MCP Server Tests: 🔄 In Progress
50 | - Integration Tests: 🔄 Pending
51 | - Documentation: 🔄 Pending
52 | - Final Review: 🔄 Pending
53 | 
```

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

```typescript
 1 | import { scopeRegistry } from '../../../modules/tools/scope-registry.js';
 2 | import { DRIVE_SCOPES, registerDriveScopes, validateDriveScopes } from '../scopes.js';
 3 | 
 4 | describe('Drive Scopes', () => {
 5 |   beforeEach(() => {
 6 |     // Clear any existing scopes
 7 |     jest.clearAllMocks();
 8 |   });
 9 | 
10 |   describe('registerDriveScopes', () => {
11 |     it('should register all drive scopes', () => {
12 |       registerDriveScopes();
13 |       const registeredScopes = scopeRegistry.getAllScopes();
14 |       
15 |       expect(registeredScopes).toContain(DRIVE_SCOPES.FULL);
16 |       expect(registeredScopes).toContain(DRIVE_SCOPES.READONLY);
17 |       expect(registeredScopes).toContain(DRIVE_SCOPES.FILE);
18 |       expect(registeredScopes).toContain(DRIVE_SCOPES.METADATA);
19 |       expect(registeredScopes).toContain(DRIVE_SCOPES.APPDATA);
20 |     });
21 |   });
22 | 
23 |   describe('validateDriveScopes', () => {
24 |     it('should return true for valid scopes', () => {
25 |       const validScopes = [
26 |         DRIVE_SCOPES.FULL,
27 |         DRIVE_SCOPES.READONLY,
28 |         DRIVE_SCOPES.FILE
29 |       ];
30 |       expect(validateDriveScopes(validScopes)).toBe(true);
31 |     });
32 | 
33 |     it('should return false for invalid scopes', () => {
34 |       const invalidScopes = [
35 |         DRIVE_SCOPES.FULL,
36 |         'invalid.scope',
37 |         DRIVE_SCOPES.FILE
38 |       ];
39 |       expect(validateDriveScopes(invalidScopes)).toBe(false);
40 |     });
41 | 
42 |     it('should return true for empty scope array', () => {
43 |       expect(validateDriveScopes([])).toBe(true);
44 |     });
45 |   });
46 | });
47 | 
```

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

```json
 1 | {
 2 |   "name": "google-workspace-mcp",
 3 |   "version": "1.2.0",
 4 |   "description": "Google Workspace OAuth MCP Server for Google Workspace integration",
 5 |   "private": true,
 6 |   "type": "module",
 7 |   "bin": {
 8 |     "google-workspace-mcp": "./build/index.js"
 9 |   },
10 |   "files": [
11 |     "build"
12 |   ],
13 |   "scripts": {
14 |     "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')); })\"",
15 |     "type-check": "tsc --noEmit",
16 |     "lint": "eslint \"src/**/*.ts\"",
17 |     "watch": "tsc --watch",
18 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js",
19 |     "start": "node build/index.js",
20 |     "setup": "node build/scripts/setup-google-env.js",
21 |     "test": "jest --config jest.config.cjs",
22 |     "test:watch": "jest --config jest.config.cjs --watch"
23 |   },
24 |   "dependencies": {
25 |     "@modelcontextprotocol/sdk": "^0.7.0",
26 |     "express": "^4.18.2",
27 |     "google-auth-library": "^9.4.1",
28 |     "googleapis": "^129.0.0",
29 |     "uuid": "^11.1.0"
30 |   },
31 |   "overrides": {
32 |     "glob": "^11.0.1"
33 |   },
34 |   "devDependencies": {
35 |     "@babel/preset-env": "^7.26.7",
36 |     "@types/express": "^4.17.21",
37 |     "@types/jest": "^29.5.14",
38 |     "@types/node": "^20.11.24",
39 |     "@types/uuid": "^10.0.0",
40 |     "@typescript-eslint/eslint-plugin": "^6.21.0",
41 |     "@typescript-eslint/parser": "^6.21.0",
42 |     "babel-jest": "^29.7.0",
43 |     "eslint": "^8.56.0",
44 |     "jest": "^29.7.0",
45 |     "lru-cache": "^11.0.2",
46 |     "ts-jest": "^29.2.5",
47 |     "uuid": "^11.1.0"
48 |   }
49 | }
50 | 
```

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

```markdown
 1 | # Active Context
 2 | 
 3 | ## Current Task
 4 | Testing and verification of the MCP server implementation, focusing on the dev-add-tests branch.
 5 | 
 6 | ## Recent Changes
 7 | 1. Fixed Account Manager Tests:
 8 |    - Fixed loadAccounts functionality with proper Map structure handling
 9 |    - Added proper initialization in validateAccount tests
10 |    - Added better error handling with specific AccountError types
11 |    - Fixed fs mocks including mkdir and dirname
12 | 
13 | 2. Fixed Calendar Service Tests:
14 |    - Updated date format expectations to match ISO string format
15 |    - Fixed createEvent response validation
16 |    - Added comprehensive tests for optional parameters
17 |    - Added tests for invalid date handling
18 | 
19 | 3. Improved Error Handling:
20 |    - Added better error handling in OAuth client
21 |    - Added debug logging for auth config loading
22 |    - Fixed error message expectations in tests
23 | 
24 | ## Test Coverage Status
25 | - Account manager: 14 tests passing
26 | - Calendar service: 11 tests passing
27 | - Gmail service: 6 tests passing
28 | - Total: 31 tests passing across all suites
29 | 
30 | ## Next Steps
31 | 1. Add more test cases:
32 |    - Edge conditions and error scenarios
33 |    - Token refresh flows
34 |    - Rate limiting handling
35 |    - Invalid input handling
36 |    - Concurrent operations
37 | 
38 | 2. Test MCP server operations:
39 |    - Tool registration
40 |    - Request validation
41 |    - Error propagation
42 |    - Authentication flows
43 |    - Response formatting
44 | 
45 | 3. Review and improve:
46 |    - Error messages clarity
47 |    - Test organization
48 |    - Mock implementations
49 |    - Documentation coverage
50 | 
51 | 4. Final Steps:
52 |    - Complete thorough testing
53 |    - Review test coverage
54 |    - Merge dev-add-tests to main
55 | 
```

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

```markdown
 1 | ## Description
 2 | Please include a summary of the changes and which issue is fixed. Include relevant motivation and context.
 3 | 
 4 | Fixes # (issue)
 5 | 
 6 | ## Type of Change
 7 | Please delete options that are not relevant.
 8 | 
 9 | - [ ] Bug fix (non-breaking change which fixes an issue)
10 | - [ ] New feature (non-breaking change which adds functionality)
11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
12 | - [ ] Documentation update
13 | - [ ] Performance improvement
14 | - [ ] Code refactoring
15 | - [ ] Test coverage improvement
16 | 
17 | ## Testing Performed
18 | Please describe the tests you ran to verify your changes:
19 | 
20 | 1. Unit Tests:
21 |    - [ ] Added new tests for new functionality
22 |    - [ ] All existing tests pass
23 |    - [ ] Test coverage maintained/improved
24 | 
25 | 2. Integration Testing:
26 |    - [ ] Tested with real Google Workspace accounts
27 |    - [ ] Verified OAuth flow
28 |    - [ ] Checked error handling
29 | 
30 | ## Checklist
31 | - [ ] My code follows the style guidelines of this project
32 | - [ ] I have performed a self-review of my code
33 | - [ ] I have commented my code, particularly in hard-to-understand areas
34 | - [ ] I have made corresponding changes to the documentation
35 | - [ ] My changes generate no new warnings
36 | - [ ] I have updated the CHANGELOG.md file
37 | - [ ] I have added tests that prove my fix is effective or that my feature works
38 | - [ ] New and existing unit tests pass locally with my changes
39 | 
40 | ## Security Considerations
41 | - [ ] I have followed security best practices
42 | - [ ] No sensitive information is exposed
43 | - [ ] API keys and tokens are properly handled
44 | - [ ] Input validation is implemented where necessary
45 | 
46 | ## Additional Notes
47 | Add any other context about the pull request here.
48 | 
```

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

```markdown
 1 | # Automatic OAuth Flow
 2 | 
 3 | This document describes the automatic OAuth authentication flow implemented in PR #[number].
 4 | 
 5 | ## Problem
 6 | 
 7 | 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.
 8 | 
 9 | ## Solution
10 | 
11 | The OAuth callback server now automatically submits the authorization code back to itself, allowing the authentication to complete transparently.
12 | 
13 | ### How It Works
14 | 
15 | 1. **User initiates authentication**
16 |    ```
17 |    authenticate_workspace_account email="[email protected]"
18 |    ```
19 | 
20 | 2. **Server returns auth URL**
21 |    - The callback server is already listening on localhost:8080
22 |    - User clicks the auth URL to open Google sign-in
23 | 
24 | 3. **User authorizes in browser**
25 |    - Google redirects to `http://localhost:8080/?code=AUTH_CODE`
26 |    - The callback page automatically POSTs the code to `/complete-auth`
27 | 
28 | 4. **Automatic completion**
29 |    - User calls `complete_workspace_auth email="[email protected]"`
30 |    - This waits for the callback server to receive the code
31 |    - Authentication completes automatically
32 | 
33 | ### Fallback Mode
34 | 
35 | For compatibility, the manual flow still works:
36 | - Set `auto_complete=false` in authenticate_workspace_account
37 | - Copy the code from the success page
38 | - Provide it with `auth_code` parameter
39 | 
40 | ### Technical Details
41 | 
42 | - Added `/complete-auth` endpoint to callback server
43 | - JavaScript in the success page automatically submits the code
44 | - The `complete_workspace_auth` tool waits for the promise to resolve
45 | - 2-minute timeout prevents hanging
46 | 
47 | ### User Experience
48 | 
49 | The new flow shows:
50 | 1. "Authentication initiated" message
51 | 2. Success page with "Completing authentication automatically..."
52 | 3. No manual code copying required
```

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

```typescript
 1 | /**
 2 |  * Simple registry to collect OAuth scopes needed by tools.
 3 |  * Scopes are gathered at startup and used for initial auth only.
 4 |  * No validation is performed - auth issues are handled via 401 responses.
 5 |  */
 6 | 
 7 | export interface ToolScope {
 8 |   scope: string;
 9 |   tool: string;
10 | }
11 | 
12 | export class ScopeRegistry {
13 |   private static instance: ScopeRegistry;
14 |   private scopes: Map<string, ToolScope>;
15 |   private scopeOrder: string[]; // Maintain registration order
16 | 
17 |   private constructor() {
18 |     this.scopes = new Map();
19 |     this.scopeOrder = [];
20 |   }
21 | 
22 |   static getInstance(): ScopeRegistry {
23 |     if (!ScopeRegistry.instance) {
24 |       ScopeRegistry.instance = new ScopeRegistry();
25 |     }
26 |     return ScopeRegistry.instance;
27 |   }
28 | 
29 |   /**
30 |    * Register a scope needed by a tool.
31 |    * If the scope is already registered, it will not be re-registered
32 |    * but its position in the order will be updated.
33 |    */
34 |   registerScope(tool: string, scope: string) {
35 |     // Remove from order if already exists
36 |     const existingIndex = this.scopeOrder.indexOf(scope);
37 |     if (existingIndex !== -1) {
38 |       this.scopeOrder.splice(existingIndex, 1);
39 |     }
40 | 
41 |     // Add to map and order
42 |     this.scopes.set(scope, { scope, tool });
43 |     this.scopeOrder.push(scope);
44 |   }
45 | 
46 |   /**
47 |    * Get all registered scopes in their registration order.
48 |    * This order is important for auth URL generation to ensure
49 |    * consistent scope presentation to users.
50 |    */
51 |   getAllScopes(): string[] {
52 |     // Return scopes in registration order
53 |     return this.scopeOrder;
54 |   }
55 | 
56 |   getToolScopes(tool: string): string[] {
57 |     return Array.from(this.scopes.values())
58 |       .filter(scope => scope.tool === tool)
59 |       .map(scope => scope.scope);
60 |   }
61 | 
62 | }
63 | 
64 | // Export a singleton instance
65 | export const scopeRegistry = ScopeRegistry.getInstance();
66 | 
```

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

```typescript
 1 | /**
 2 |  * Parameters for retrieving contacts.
 3 |  */
 4 | export interface GetContactsParams {
 5 |   email: string; // The user account email
 6 |   pageSize?: number; // Max number of contacts to return
 7 |   pageToken?: string; // Token for pagination
 8 |   // Add other parameters like sortOrder, syncToken if needed later
 9 |   personFields: string; // Required: Fields to request e.g. 'names,emailAddresses,phoneNumbers'
10 | }
11 | 
12 | /**
13 |  * Response structure for getting contacts.
14 |  */
15 | export interface GetContactsResponse {
16 |   connections: Contact[];
17 |   nextPageToken?: string;
18 |   totalPeople?: number;
19 |   totalItems?: number; // Deprecated
20 | }
21 | 
22 | /**
23 |  * Represents a Google Contact (Person).
24 |  * Based on People API Person resource.
25 |  * Reference: https://developers.google.com/people/api/rest/v1/people#Person
26 |  */
27 | export interface Contact {
28 |   resourceName: string;
29 |   etag?: string;
30 |   names?: Name[];
31 |   emailAddresses?: EmailAddress[];
32 |   phoneNumbers?: PhoneNumber[];
33 |   // Add other fields as needed (e.g. photos, addresses, organizations, etc.)
34 | }
35 | 
36 | // --- Sub-types based on People API ---
37 | 
38 | export interface Name {
39 |   displayName?: string;
40 |   familyName?: string;
41 |   givenName?: string;
42 |   // ... other name fields
43 | }
44 | 
45 | export interface EmailAddress {
46 |   value?: string;
47 |   type?: string; // e.g. 'home', 'work'
48 |   formattedType?: string;
49 |   // ... other email fields
50 | }
51 | 
52 | export interface PhoneNumber {
53 |   value?: string;
54 |   canonicalForm?: string;
55 |   type?: string; // e.g. 'mobile', 'home', 'work'
56 |   formattedType?: string;
57 |   // ... other phone fields
58 | }
59 | 
60 | /**
61 |  * Base error class for Contacts service.
62 |  */
63 | export class ContactsError extends Error {
64 |   code: string;
65 |   details?: string;
66 | 
67 |   constructor(message: string, code: string, details?: string) {
68 |     super(message);
69 |     this.name = "ContactsError";
70 |     this.code = code;
71 |     this.details = details;
72 |   }
73 | }
74 | 
```

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

```typescript
 1 | // Account Types
 2 | export interface Account {
 3 |   email: string;
 4 |   category: string;
 5 |   description: string;
 6 |   auth_status?: {
 7 |     has_token: boolean;
 8 |     scopes?: string[];
 9 |     expires?: number;
10 |   };
11 | }
12 | 
13 | export interface AccountsConfig {
14 |   accounts: Account[];
15 | }
16 | 
17 | // Token Types
18 | export interface TokenData {
19 |   access_token: string;
20 |   refresh_token: string;
21 |   scope: string;
22 |   token_type: string;
23 |   expiry_date: number;
24 |   last_refresh: number;
25 | }
26 | 
27 | // OAuth Config Types
28 | export interface OAuthConfig {
29 |   client_id: string;
30 |   client_secret: string;
31 |   auth_uri: string;
32 |   token_uri: string;
33 | }
34 | 
35 | // Authentication Types
36 | export interface GoogleAuthParams {
37 |   email: string;
38 |   category?: string;
39 |   description?: string;
40 |   required_scopes: string[];
41 |   auth_code?: string;
42 | }
43 | 
44 | // API Request Types
45 | export interface GoogleApiRequestParams extends GoogleAuthParams {
46 |   api_endpoint: string;
47 |   method: 'GET' | 'POST' | 'PUT' | 'DELETE';
48 |   params?: Record<string, any>;
49 | }
50 | 
51 | // API Response Types
52 | export type GoogleApiResponse = 
53 |   | {
54 |       status: 'success';
55 |       data?: any;
56 |       message?: string;
57 |     }
58 |   | {
59 |       status: 'auth_required';
60 |       auth_url: string;
61 |       message?: string;
62 |       instructions: string;
63 |     }
64 |   | {
65 |       status: 'refreshing';
66 |       message: string;
67 |     }
68 |   | {
69 |       status: 'error';
70 |       error: string;
71 |       message?: string;
72 |       resolution?: string;
73 |     };
74 | 
75 | // Error Types
76 | export class GoogleApiError extends Error {
77 |   constructor(
78 |     message: string,
79 |     public readonly code: string,
80 |     public readonly resolution?: string
81 |   ) {
82 |     super(message);
83 |     this.name = 'GoogleApiError';
84 |   }
85 | }
86 | 
87 | // Utility Types
88 | export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
89 | 
90 | export interface ApiRequestParams {
91 |   endpoint: string;
92 |   method: HttpMethod;
93 |   params?: Record<string, any>;
94 |   token: string;
95 | }
96 | 
```

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

```yaml
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ main, fix/* ]
 6 |   pull_request:
 7 |     branches: [ main ]
 8 |   workflow_dispatch:
 9 | 
10 | env:
11 |   BUILDX_NO_DEFAULT_LOAD: true
12 | 
13 | jobs:
14 |   test:
15 |     runs-on: ubuntu-latest
16 | 
17 |     steps:
18 |     - uses: actions/checkout@v3
19 | 
20 |     - name: Use Node.js
21 |       uses: actions/setup-node@v3
22 |       with:
23 |         node-version: '20.x'
24 |         cache: 'npm'
25 | 
26 |     - name: Install dependencies
27 |       run: npm ci
28 | 
29 |     - name: Run tests
30 |       run: npm test
31 |       env:
32 |         GOOGLE_CLIENT_ID: test-id
33 |         GOOGLE_CLIENT_SECRET: test-secret
34 |         CONFIG_DIR: ./test-config
35 |         WORKSPACE_BASE_PATH: ./test-workspace
36 | 
37 |   build:
38 |     runs-on: ubuntu-latest
39 | 
40 |     steps:
41 |     - uses: actions/checkout@v3
42 | 
43 |     - name: Use Node.js
44 |       uses: actions/setup-node@v3
45 |       with:
46 |         node-version: '20.x'
47 |         cache: 'npm'
48 | 
49 |     - name: Install dependencies
50 |       run: npm ci
51 | 
52 |     - name: Build
53 |       run: npm run build
54 | 
55 |     - name: Set up Docker Buildx
56 |       uses: docker/setup-buildx-action@v3
57 |       with:
58 |         platforms: linux/amd64,linux/arm64
59 | 
60 |     - name: Test Docker build
61 |       uses: docker/build-push-action@v5
62 |       with:
63 |         context: .
64 |         push: false
65 |         load: false
66 |         tags: google-workspace-mcp:test
67 |         platforms: linux/amd64,linux/arm64
68 |         cache-from: |
69 |           type=gha,scope=${{ github.ref_name }}-amd64
70 |           type=gha,scope=${{ github.ref_name }}-arm64
71 |         cache-to: |
72 |           type=gha,mode=min,scope=${{ github.ref_name }}-amd64
73 |           type=gha,mode=min,scope=${{ github.ref_name }}-arm64
74 |         outputs: type=image,name=google-workspace-mcp:test
75 | 
76 |   lint:
77 |     runs-on: ubuntu-latest
78 | 
79 |     steps:
80 |     - uses: actions/checkout@v3
81 | 
82 |     - name: Use Node.js
83 |       uses: actions/setup-node@v3
84 |       with:
85 |         node-version: '20.x'
86 |         cache: 'npm'
87 | 
88 |     - name: Install dependencies
89 |       run: npm ci
90 | 
91 |     - name: Run ESLint
92 |       run: npm run lint
93 | 
```

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

```typescript
 1 | import logger from './logger.js';
 2 | import { initializeAccountModule } from '../modules/accounts/index.js';
 3 | import { initializeGmailModule } from '../modules/gmail/index.js';
 4 | import { initializeCalendarModule } from '../modules/calendar/index.js';
 5 | import { initializeDriveModule } from '../modules/drive/index.js';
 6 | import { initializeContactsModule } from '../modules/contacts/index.js';
 7 | import { registerGmailScopes } from '../modules/gmail/scopes.js';
 8 | import { registerCalendarScopes } from '../modules/calendar/scopes.js';
 9 | import { registerDriveScopes } from '../modules/drive/scopes.js';
10 | import { CONTACTS_SCOPES } from '../modules/contacts/scopes.js';
11 | import { scopeRegistry } from '../modules/tools/scope-registry.js';
12 | 
13 | // Function to register contacts scopes
14 | function registerContactsScopes(): void {
15 |   scopeRegistry.registerScope("contacts", CONTACTS_SCOPES.READONLY);
16 |   logger.info('Contacts scopes registered');
17 | }
18 | 
19 | export async function initializeAllServices(): Promise<void> {
20 |   try {
21 |     // Register all scopes first
22 |     logger.info('Registering API scopes...');
23 |     registerGmailScopes();
24 |     registerCalendarScopes();
25 |     registerDriveScopes();
26 |     registerContactsScopes();
27 | 
28 |     // Initialize account module first as other services depend on it
29 |     logger.info('Initializing account module...');
30 |     await initializeAccountModule();
31 | 
32 |     // Initialize remaining services in parallel
33 |     logger.info('Initializing service modules in parallel...');
34 |     await Promise.all([
35 |       initializeDriveModule().then(() => logger.info('Drive module initialized')),
36 |       initializeGmailModule().then(() => logger.info('Gmail module initialized')),
37 |       initializeCalendarModule().then(() => logger.info('Calendar module initialized')),
38 |       initializeContactsModule().then(() => logger.info('Contacts module initialized'))
39 |     ]);
40 | 
41 |     logger.info('All services initialized successfully');
42 |   } catch (error) {
43 |     logger.error('Failed to initialize services:', error);
44 |     throw error;
45 |   }
46 | }
47 | 
```

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

```typescript
 1 | import { scopeRegistry } from '../tools/scope-registry.js';
 2 | 
 3 | // Define Gmail scopes as constants for reuse and testing
 4 | // Reference: https://developers.google.com/gmail/api/auth/scopes
 5 | export const GMAIL_SCOPES = {
 6 |   // Core functionality scopes (read, write, modify permissions)
 7 |   READONLY: 'https://www.googleapis.com/auth/gmail.readonly',
 8 |   SEND: 'https://www.googleapis.com/auth/gmail.send',
 9 |   MODIFY: 'https://www.googleapis.com/auth/gmail.modify',
10 |   
11 |   // Label management scope
12 |   LABELS: 'https://www.googleapis.com/auth/gmail.labels',
13 |   
14 |   // Settings management scopes
15 |   SETTINGS_BASIC: 'https://www.googleapis.com/auth/gmail.settings.basic',
16 |   SETTINGS_SHARING: 'https://www.googleapis.com/auth/gmail.settings.sharing'
17 | };
18 | 
19 | /**
20 |  * Register Gmail OAuth scopes at startup.
21 |  * Auth issues will be handled via 401 responses rather than pre-validation.
22 |  * 
23 |  * IMPORTANT: The order of scope registration matters for auth URL generation.
24 |  * Core functionality scopes (readonly, send, modify) should be registered first,
25 |  * followed by feature-specific scopes (labels, settings).
26 |  */
27 | export function registerGmailScopes() {
28 |   // Register core functionality scopes first
29 |   scopeRegistry.registerScope('gmail', GMAIL_SCOPES.READONLY);
30 |   scopeRegistry.registerScope('gmail', GMAIL_SCOPES.SEND);
31 |   scopeRegistry.registerScope('gmail', GMAIL_SCOPES.MODIFY);
32 |   
33 |   // Register feature-specific scopes
34 |   scopeRegistry.registerScope('gmail', GMAIL_SCOPES.LABELS);
35 |   
36 |   // Register settings scopes last (order matters for auth URL generation)
37 |   scopeRegistry.registerScope('gmail', GMAIL_SCOPES.SETTINGS_BASIC);
38 |   scopeRegistry.registerScope('gmail', GMAIL_SCOPES.SETTINGS_SHARING);
39 |   
40 |   // Verify all scopes are registered
41 |   const registeredScopes = scopeRegistry.getAllScopes();
42 |   const requiredScopes = Object.values(GMAIL_SCOPES);
43 |   
44 |   const missingScopes = requiredScopes.filter(scope => !registeredScopes.includes(scope));
45 |   if (missingScopes.length > 0) {
46 |     throw new Error(`Failed to register Gmail scopes: ${missingScopes.join(', ')}`);
47 |   }
48 | }
49 | 
```

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

```typescript
 1 | import { scopeRegistry } from '../tools/scope-registry.js';
 2 | 
 3 | // Define Drive scopes as constants for reuse and testing
 4 | // Reference: https://developers.google.com/drive/api/auth/scopes
 5 | export const DRIVE_SCOPES = {
 6 |   // Full access to files and folders (create, read, update, delete)
 7 |   FULL: 'https://www.googleapis.com/auth/drive',
 8 |   
 9 |   // Read-only access to files
10 |   READONLY: 'https://www.googleapis.com/auth/drive.readonly',
11 |   
12 |   // Access to files created by the app
13 |   FILE: 'https://www.googleapis.com/auth/drive.file',
14 |   
15 |   // Access to metadata only
16 |   METADATA: 'https://www.googleapis.com/auth/drive.metadata',
17 |   
18 |   // Access to app data folder
19 |   APPDATA: 'https://www.googleapis.com/auth/drive.appdata',
20 | } as const;
21 | 
22 | export type DriveScope = typeof DRIVE_SCOPES[keyof typeof DRIVE_SCOPES];
23 | 
24 | /**
25 |  * Register Drive OAuth scopes at startup.
26 |  * Auth issues will be handled via 401 responses rather than pre-validation.
27 |  * 
28 |  * IMPORTANT: The order of scope registration matters for auth URL generation.
29 |  * Core functionality scopes should be registered first,
30 |  * followed by feature-specific scopes.
31 |  */
32 | export function registerDriveScopes(): void {
33 |   // Register core functionality scopes first
34 |   scopeRegistry.registerScope('drive', DRIVE_SCOPES.FULL);
35 |   scopeRegistry.registerScope('drive', DRIVE_SCOPES.READONLY);
36 |   scopeRegistry.registerScope('drive', DRIVE_SCOPES.FILE);
37 |   
38 |   // Register feature-specific scopes
39 |   scopeRegistry.registerScope('drive', DRIVE_SCOPES.METADATA);
40 |   scopeRegistry.registerScope('drive', DRIVE_SCOPES.APPDATA);
41 |   
42 |   // Verify all scopes are registered
43 |   const registeredScopes = scopeRegistry.getAllScopes();
44 |   const requiredScopes = Object.values(DRIVE_SCOPES);
45 |   
46 |   const missingScopes = requiredScopes.filter(scope => !registeredScopes.includes(scope));
47 |   if (missingScopes.length > 0) {
48 |     throw new Error(`Failed to register Drive scopes: ${missingScopes.join(', ')}`);
49 |   }
50 | }
51 | 
52 | export function getDriveScopes(): string[] {
53 |   return Object.values(DRIVE_SCOPES);
54 | }
55 | 
56 | export function validateDriveScopes(scopes: string[]): boolean {
57 |   const validScopes = new Set(getDriveScopes());
58 |   return scopes.every(scope => validScopes.has(scope));
59 | }
60 | 
```

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

```typescript
 1 | /**
 2 |  * Calendar Module Entry Point
 3 |  * 
 4 |  * This module provides Google Calendar integration following the same singleton
 5 |  * pattern as the Gmail module. It handles:
 6 |  * - Module initialization with OAuth setup
 7 |  * - Calendar service instance management
 8 |  * - Type and error exports
 9 |  * 
10 |  * Usage:
11 |  * ```typescript
12 |  * // Initialize the module
13 |  * await initializeCalendarModule();
14 |  * 
15 |  * // Get service instance
16 |  * const calendarService = getCalendarService();
17 |  * 
18 |  * // Use calendar operations
19 |  * const events = await calendarService.getEvents({
20 |  *   email: '[email protected]',
21 |  *   maxResults: 10
22 |  * });
23 |  * ```
24 |  */
25 | 
26 | import { CalendarService } from './service.js';
27 | import {
28 |   GetEventsParams,
29 |   CreateEventParams,
30 |   EventResponse,
31 |   CreateEventResponse,
32 |   CalendarError,
33 |   CalendarModuleConfig
34 | } from './types.js';
35 | 
36 | // Create singleton instance
37 | let calendarService: CalendarService | null = null;
38 | 
39 | /**
40 |  * Initialize the Calendar module
41 |  * This must be called before using any calendar operations
42 |  * 
43 |  * @param config - Optional configuration including OAuth scope overrides
44 |  * @returns Initialized CalendarService instance
45 |  * 
46 |  * Note: This function ensures only one instance of the service exists,
47 |  * following the singleton pattern for consistent state management.
48 |  */
49 | export async function initializeCalendarModule(config?: CalendarModuleConfig): Promise<CalendarService> {
50 |   if (!calendarService) {
51 |     calendarService = new CalendarService(config);
52 |     await calendarService.initialize();
53 |   }
54 |   return calendarService;
55 | }
56 | 
57 | /**
58 |  * Get the initialized Calendar service instance
59 |  * 
60 |  * @returns CalendarService instance
61 |  * @throws CalendarError if the module hasn't been initialized
62 |  * 
63 |  * Note: Always call initializeCalendarModule before using this function
64 |  */
65 | export function getCalendarService(): CalendarService {
66 |   if (!calendarService) {
67 |     throw new CalendarError(
68 |       'Calendar module not initialized',
69 |       'MODULE_NOT_INITIALIZED',
70 |       'Call initializeCalendarModule before using the Calendar service'
71 |     );
72 |   }
73 |   return calendarService;
74 | }
75 | 
76 | export {
77 |   CalendarService,
78 |   GetEventsParams,
79 |   CreateEventParams,
80 |   EventResponse,
81 |   CreateEventResponse,
82 |   CalendarError,
83 |   CalendarModuleConfig
84 | };
85 | 
```

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

```markdown
 1 | # System Patterns
 2 | 
 3 | ## Architecture
 4 | The system follows a modular architecture with clear separation of concerns:
 5 | 
 6 | ### Core Components
 7 | 1. **Scope Registry** (src/modules/tools/scope-registry.ts)
 8 |    - Simple scope collection system
 9 |    - Gathers required scopes at startup
10 |    - Used only for initial auth setup
11 | 
12 | 2. **MCP Server** (src/index.ts)
13 |    - Registers and manages available tools
14 |    - Handles request routing and validation
15 |    - Provides consistent error handling
16 | 
17 | 3. **Account Module** (src/modules/accounts/*)
18 |    - OAuth Client: Implements Google OAuth 2.0 flow
19 |    - Token Manager: Handles token lifecycle
20 |    - Account Manager: Manages account configurations
21 | 
22 | 4. **Service Modules**
23 |    - Gmail Module: Implements email operations
24 |    - Calendar Module: Handles calendar operations
25 | 
26 | ## Key Technical Decisions
27 | 1. **Simplest Viable Design**
28 |    - Minimize complexity in permission structures
29 |    - Handle auth through HTTP response codes (401/403)
30 |    - Move OAuth mechanics into platform infrastructure
31 |    - Present simple verb-noun interfaces
32 | 
33 | 2. **Tool Registration Pattern**
34 |    - Tools must be registered in both ListToolsRequestSchema and CallToolRequestSchema
35 |    - Follows verb-noun naming convention (e.g., list_workspace_accounts)
36 | 
37 | 3. **Error Handling**
38 |    - Simplified auth error handling through 401/403 responses
39 |    - Automatic token refresh on auth failures
40 |    - Service-specific error types
41 |    - Clear authentication error guidance
42 | 
43 | ## Project Structure
44 | ```
45 | src/
46 | ├── index.ts                 # MCP server implementation
47 | ├── modules/
48 | │   ├── accounts/           # Account & auth handling
49 | │   │   ├── index.ts       # Module entry point
50 | │   │   ├── manager.ts     # Account management
51 | │   │   ├── oauth.ts       # OAuth implementation
52 | │   │   └── token.ts       # Token handling
53 | │   └── gmail/             # Gmail implementation
54 | │       ├── index.ts       # Module entry point
55 | │       ├── service.ts     # Gmail operations
56 | │       └── types.ts       # Gmail types
57 | └── scripts/
58 |     └── setup-google-env.ts # Setup utilities
59 | ```
60 | 
61 | ## Configuration Patterns
62 | - Environment-based file paths
63 | - Separate credential storage
64 | - Account configuration management
65 | - Token persistence handling
66 | - Security through proper credential and token management
67 | 
```

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

```typescript
 1 | import { AttachmentIndexService } from './index-service.js';
 2 | 
 3 | /**
 4 |  * Simplified attachment information visible to AI
 5 |  */
 6 | export interface AttachmentInfo {
 7 |   name: string;
 8 | }
 9 | 
10 | /**
11 |  * Service for transforming API responses to hide complex attachment IDs from AI
12 |  */
13 | export class AttachmentResponseTransformer {
14 |   constructor(private indexService: AttachmentIndexService) {}
15 | 
16 |   /**
17 |    * Transform a response object that may contain attachments
18 |    * Works with both email and calendar responses
19 |    */
20 |   /**
21 |    * Transform a response object that may contain attachments
22 |    * Works with both email and calendar responses
23 |    */
24 |   transformResponse<T>(response: T): T {
25 |     if (Array.isArray(response)) {
26 |       return response.map(item => this.transformResponse(item)) as unknown as T;
27 |     }
28 | 
29 |     if (typeof response !== 'object' || response === null) {
30 |       return response;
31 |     }
32 | 
33 |     // Deep clone to avoid modifying original
34 |     const transformed = { ...response } as Record<string, any>;
35 | 
36 |     // Transform attachments if present
37 |     if ('attachments' in transformed && 
38 |         Array.isArray(transformed.attachments) && 
39 |         'id' in transformed) {
40 |       const messageId = transformed.id as string;
41 |       
42 |       // Store full metadata in index
43 |       transformed.attachments.forEach((attachment: any) => {
44 |         if (attachment?.id && attachment?.name) {
45 |           this.indexService.addAttachment(messageId, {
46 |             id: attachment.id,
47 |             name: attachment.name,
48 |             mimeType: attachment.mimeType || 'application/octet-stream',
49 |             size: attachment.size || 0
50 |           });
51 |         }
52 |       });
53 | 
54 |       // Replace with simplified version
55 |       transformed.attachments = transformed.attachments.map((attachment: any) => ({
56 |         name: attachment?.name || 'Unknown file'
57 |       }));
58 |     }
59 | 
60 |     // Recursively transform nested objects
61 |     Object.keys(transformed).forEach(key => {
62 |       if (typeof transformed[key] === 'object' && transformed[key] !== null) {
63 |         transformed[key] = this.transformResponse(transformed[key]);
64 |       }
65 |     });
66 | 
67 |     return transformed as unknown as T;
68 |   }
69 | 
70 |   /**
71 |    * Create a refresh placeholder for expired attachments
72 |    */
73 |   static createRefreshPlaceholder(): AttachmentInfo[] {
74 |     return [{
75 |       name: "Attachments expired - Request message again to view"
76 |     }];
77 |   }
78 | }
79 | 
```

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

```typescript
 1 | import { scopeRegistry } from '../tools/scope-registry.js';
 2 | 
 3 | // Define Calendar scopes as constants for reuse and testing
 4 | // Reference: https://developers.google.com/calendar/api/auth
 5 | export const CALENDAR_SCOPES = {
 6 |   // Core functionality scopes
 7 |   READONLY: 'https://www.googleapis.com/auth/calendar.readonly',  // Required for reading calendars and events
 8 |   EVENTS: 'https://www.googleapis.com/auth/calendar.events',      // Required for creating/updating events
 9 |   EVENTS_READONLY: 'https://www.googleapis.com/auth/calendar.events.readonly',  // Required for reading events only
10 |   
11 |   // Settings scopes
12 |   SETTINGS_READONLY: 'https://www.googleapis.com/auth/calendar.settings.readonly',  // Required for reading calendar settings
13 |   
14 |   // Full access scope (includes all above permissions)
15 |   FULL_ACCESS: 'https://www.googleapis.com/auth/calendar'         // Complete calendar access
16 | };
17 | 
18 | /**
19 |  * Register Calendar OAuth scopes at startup.
20 |  * Auth issues will be handled via 401 responses rather than pre-validation.
21 |  * 
22 |  * IMPORTANT: The order of scope registration matters for auth URL generation.
23 |  * Core functionality scopes (readonly) should be registered first,
24 |  * followed by feature-specific scopes (events), and settings scopes last.
25 |  */
26 | export function registerCalendarScopes() {
27 |   // Register core functionality scopes first (order matters for auth URL generation)
28 |   scopeRegistry.registerScope('calendar', CALENDAR_SCOPES.READONLY);   // For reading calendars and events
29 |   scopeRegistry.registerScope('calendar', CALENDAR_SCOPES.EVENTS);     // For managing calendar events
30 |   scopeRegistry.registerScope('calendar', CALENDAR_SCOPES.EVENTS_READONLY);  // For reading events only
31 |   
32 |   // Register settings scopes
33 |   scopeRegistry.registerScope('calendar', CALENDAR_SCOPES.SETTINGS_READONLY);  // For reading calendar settings
34 |   
35 |   // Register full access scope last
36 |   scopeRegistry.registerScope('calendar', CALENDAR_SCOPES.FULL_ACCESS);  // Complete calendar access (includes all above)
37 |   
38 |   // Verify all scopes are registered
39 |   const registeredScopes = scopeRegistry.getAllScopes();
40 |   const requiredScopes = Object.values(CALENDAR_SCOPES);
41 |   
42 |   const missingScopes = requiredScopes.filter(scope => !registeredScopes.includes(scope));
43 |   if (missingScopes.length > 0) {
44 |     throw new Error(`Failed to register Calendar scopes: ${missingScopes.join(', ')}`);
45 |   }
46 | }
47 | 
```

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

```typescript
  1 | import { AttachmentMetadata } from '../attachments/types.js';
  2 | import { AttachmentInfo } from '../attachments/response-transformer.js';
  3 | 
  4 | export interface CalendarModuleConfig {
  5 |   maxAttachmentSize?: number;
  6 |   allowedAttachmentTypes?: string[];
  7 | }
  8 | 
  9 | export interface CalendarAttachment {
 10 |   content: string;      // Base64 content
 11 |   title: string;       // Filename
 12 |   mimeType: string;    // MIME type
 13 |   size?: number;       // Size in bytes
 14 | }
 15 | 
 16 | export interface EventTime {
 17 |   dateTime: string;
 18 |   timeZone?: string;
 19 | }
 20 | 
 21 | export interface EventAttendee {
 22 |   email: string;
 23 |   responseStatus?: string;
 24 | }
 25 | 
 26 | export interface EventOrganizer {
 27 |   email: string;
 28 |   self: boolean;
 29 | }
 30 | 
 31 | export interface EventResponse {
 32 |   id: string;
 33 |   summary: string;
 34 |   description?: string;
 35 |   start: EventTime;
 36 |   end: EventTime;
 37 |   attendees?: EventAttendee[];
 38 |   organizer?: EventOrganizer;
 39 |   attachments?: AttachmentInfo[];
 40 | }
 41 | 
 42 | export interface GetEventsParams {
 43 |   email: string;
 44 |   query?: string;
 45 |   maxResults?: number;
 46 |   timeMin?: string;
 47 |   timeMax?: string;
 48 | }
 49 | 
 50 | export interface CreateEventParams {
 51 |   email: string;
 52 |   summary: string;
 53 |   description?: string;
 54 |   start: EventTime;
 55 |   end: EventTime;
 56 |   attendees?: {
 57 |     email: string;
 58 |   }[];
 59 |   attachments?: {
 60 |     driveFileId?: string;  // For existing Drive files
 61 |     content?: string;      // Base64 content for new files
 62 |     name: string;
 63 |     mimeType: string;
 64 |     size?: number;
 65 |   }[];
 66 | }
 67 | 
 68 | export interface CreateEventResponse {
 69 |   id: string;
 70 |   summary: string;
 71 |   htmlLink: string;
 72 |   attachments?: AttachmentMetadata[];
 73 | }
 74 | 
 75 | export interface ManageEventParams {
 76 |   email: string;
 77 |   eventId: string;
 78 |   action: 'accept' | 'decline' | 'tentative' | 'propose_new_time' | 'update_time';
 79 |   comment?: string;
 80 |   newTimes?: {
 81 |     start: EventTime;
 82 |     end: EventTime;
 83 |   }[];
 84 | }
 85 | 
 86 | export interface ManageEventResponse {
 87 |   success: boolean;
 88 |   eventId: string;
 89 |   action: string;
 90 |   status: 'completed' | 'proposed' | 'updated';
 91 |   htmlLink?: string;
 92 |   proposedTimes?: {
 93 |     start: EventTime;
 94 |     end: EventTime;
 95 |   }[];
 96 | }
 97 | 
 98 | export interface DeleteEventParams {
 99 |   email: string;
100 |   eventId: string;
101 |   sendUpdates?: 'all' | 'externalOnly' | 'none';
102 |   deletionScope?: 'entire_series' | 'this_and_following';
103 | }
104 | 
105 | export class CalendarError extends Error implements CalendarError {
106 |   code: string;
107 |   details?: string;
108 | 
109 |   constructor(message: string, code: string, details?: string) {
110 |     super(message);
111 |     this.name = 'CalendarError';
112 |     this.code = code;
113 |     this.details = details;
114 |   }
115 | }
116 | 
```

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

```typescript
 1 | import { appendFileSync, existsSync, mkdirSync } from 'fs';
 2 | import { dirname } from 'path';
 3 | 
 4 | /**
 5 |  * Logger utility with configurable logging behavior.
 6 |  * 
 7 |  * Supports two logging modes:
 8 |  * - normal: Uses appropriate console methods for each log level (error, warn, info, debug)
 9 |  * - strict: Routes all non-JSON-RPC messages to stderr for compatibility with tools like Claude desktop
10 |  * 
11 |  * Configure via:
12 |  * - LOG_MODE environment variable:
13 |  *   - LOG_MODE=normal (default) - Standard logging behavior
14 |  *   - LOG_MODE=strict - All logs except JSON-RPC go to stderr
15 |  * - LOG_FILE environment variable:
16 |  *   - If set, logs will also be written to this file
17 |  * 
18 |  * For testing: The logger should be mocked in tests to prevent console noise.
19 |  * See src/__helpers__/testSetup.ts for the mock implementation.
20 |  */
21 | 
22 | type LogMode = 'normal' | 'strict';
23 | 
24 | const LOG_MODE = (process.env.LOG_MODE || 'normal') as LogMode;
25 | const LOG_FILE = process.env.LOG_FILE;
26 | 
27 | // Ensure log directory exists if LOG_FILE is set
28 | if (LOG_FILE) {
29 |   const dir = dirname(LOG_FILE);
30 |   if (!existsSync(dir)) {
31 |     mkdirSync(dir, { recursive: true });
32 |   }
33 | }
34 | 
35 | const isJsonRpc = (msg: any): boolean => {
36 |   if (typeof msg !== 'string') return false;
37 |   return msg.startsWith('{"jsonrpc":') || msg.startsWith('{"id":');
38 | };
39 | 
40 | const writeToLogFile = (level: string, ...args: any[]) => {
41 |   if (!LOG_FILE) return;
42 |   try {
43 |     const timestamp = new Date().toISOString();
44 |     const message = args.map(arg => 
45 |       typeof arg === 'string' ? arg : JSON.stringify(arg)
46 |     ).join(' ');
47 |     appendFileSync(LOG_FILE, `[${timestamp}] [${level.toUpperCase()}] ${message}\n`);
48 |   } catch (error) {
49 |     console.error('Failed to write to log file:', error);
50 |   }
51 | };
52 | 
53 | const logger = {
54 |   error: (...args: any[]) => {
55 |     console.error(...args);
56 |     writeToLogFile('error', ...args);
57 |   },
58 |   warn: (...args: any[]) => {
59 |     if (LOG_MODE === 'strict' || isJsonRpc(args[0])) {
60 |       console.error(...args);
61 |     } else {
62 |       console.warn(...args);
63 |     }
64 |     writeToLogFile('warn', ...args);
65 |   },
66 |   info: (...args: any[]) => {
67 |     if (LOG_MODE === 'strict' || isJsonRpc(args[0])) {
68 |       console.error(...args);
69 |     } else {
70 |       console.info(...args);
71 |     }
72 |     writeToLogFile('info', ...args);
73 |   },
74 |   debug: (...args: any[]) => {
75 |     if (!process.env.DEBUG) return;
76 |     if (LOG_MODE === 'strict' || isJsonRpc(args[0])) {
77 |       console.error(...args);
78 |     } else {
79 |       console.debug(...args);
80 |     }
81 |     if (process.env.DEBUG) {
82 |       writeToLogFile('debug', ...args);
83 |     }
84 |   }
85 | };
86 | 
87 | export default logger;
88 | 
```

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

```typescript
 1 | import { registerCalendarScopes, CALENDAR_SCOPES } from '../scopes.js';
 2 | import { scopeRegistry } from '../../tools/scope-registry.js';
 3 | 
 4 | describe('Calendar Scopes', () => {
 5 |   beforeEach(() => {
 6 |     // Reset the scope registry before each test
 7 |     // @ts-expect-error - accessing private property for testing
 8 |     scopeRegistry.scopes = new Map();
 9 |     // @ts-expect-error - accessing private property for testing
10 |     scopeRegistry.scopeOrder = [];
11 |   });
12 | 
13 |   it('should register all required Calendar scopes', () => {
14 |     registerCalendarScopes();
15 |     const registeredScopes = scopeRegistry.getAllScopes();
16 |     
17 |     // Required scopes for Calendar functionality
18 |     const requiredScopes = [
19 |       CALENDAR_SCOPES.READONLY,         // For viewing events
20 |       CALENDAR_SCOPES.EVENTS,           // For creating/modifying events
21 |       CALENDAR_SCOPES.SETTINGS_READONLY, // For calendar settings
22 |       CALENDAR_SCOPES.FULL_ACCESS       // For full calendar management
23 |     ];
24 | 
25 |     // Verify each required scope is registered
26 |     requiredScopes.forEach(scope => {
27 |       expect(registeredScopes).toContain(scope);
28 |     });
29 |   });
30 | 
31 |   it('should register scopes in correct order', () => {
32 |     registerCalendarScopes();
33 |     const registeredScopes = scopeRegistry.getAllScopes();
34 | 
35 |     // Core functionality scopes should come first
36 |     const coreScopes = [
37 |       CALENDAR_SCOPES.READONLY
38 |     ];
39 | 
40 |     // Feature-specific scopes should come next
41 |     const featureScopes = [
42 |       CALENDAR_SCOPES.EVENTS,
43 |       CALENDAR_SCOPES.FULL_ACCESS
44 |     ];
45 | 
46 |     // Settings scopes should come last
47 |     const settingsScopes = [
48 |       CALENDAR_SCOPES.SETTINGS_READONLY
49 |     ];
50 | 
51 |     // Verify order of scope groups
52 |     const firstCoreIndex = Math.min(...coreScopes.map(scope => registeredScopes.indexOf(scope)));
53 |     const firstFeatureIndex = Math.min(...featureScopes.map(scope => registeredScopes.indexOf(scope)));
54 |     const firstSettingsIndex = Math.min(...settingsScopes.map(scope => registeredScopes.indexOf(scope)));
55 | 
56 |     expect(firstCoreIndex).toBeLessThan(firstFeatureIndex);
57 |     expect(firstFeatureIndex).toBeLessThan(firstSettingsIndex);
58 |   });
59 | 
60 |   it('should maintain scope registration when re-registering', () => {
61 |     // Register scopes first time
62 |     registerCalendarScopes();
63 |     const initialScopes = scopeRegistry.getAllScopes();
64 | 
65 |     // Register scopes second time
66 |     registerCalendarScopes();
67 |     const finalScopes = scopeRegistry.getAllScopes();
68 | 
69 |     // Verify all scopes are still registered in same order
70 |     expect(finalScopes).toEqual(initialScopes);
71 |   });
72 | });
73 | 
```

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

```yaml
  1 | name: Test, Build and Deploy
  2 | 
  3 | on:
  4 |   push:
  5 |     branches: [ main, fix/* ]
  6 |     tags: [ 'v*' ]
  7 |   pull_request:
  8 |     branches: [ main ]
  9 | 
 10 | env:
 11 |   REGISTRY: ghcr.io
 12 |   IMAGE_NAME: ${{ github.repository }}
 13 |   NODE_VERSION: '20'
 14 | 
 15 | jobs:
 16 |   test:
 17 |     runs-on: ubuntu-latest
 18 |     steps:
 19 |       - name: Checkout repository
 20 |         uses: actions/checkout@v4
 21 | 
 22 |       - name: Set up Node.js
 23 |         uses: actions/setup-node@v4
 24 |         with:
 25 |           node-version: ${{ env.NODE_VERSION }}
 26 |           cache: 'npm'
 27 | 
 28 |       - name: Install dependencies
 29 |         run: npm ci
 30 | 
 31 |       - name: Type check
 32 |         run: npm run type-check
 33 | 
 34 |       - name: Lint
 35 |         run: npm run lint
 36 | 
 37 |       - name: Run tests
 38 |         run: npm test
 39 | 
 40 |   build-and-deploy:
 41 |     needs: test
 42 |     runs-on: ubuntu-latest
 43 |     permissions:
 44 |       contents: read
 45 |       packages: write
 46 | 
 47 |     steps:
 48 |       - name: Checkout repository
 49 |         uses: actions/checkout@v4
 50 | 
 51 |       - name: Log in to the Container registry
 52 |         uses: docker/login-action@v3
 53 |         with:
 54 |           registry: ${{ env.REGISTRY }}
 55 |           username: ${{ github.actor }}
 56 |           password: ${{ secrets.GITHUB_TOKEN }}
 57 | 
 58 |       - name: Set up QEMU
 59 |         uses: docker/setup-qemu-action@v3
 60 | 
 61 |       - name: Set up Docker Buildx
 62 |         uses: docker/setup-buildx-action@v3
 63 | 
 64 |       - name: Extract metadata (tags, labels) for Docker
 65 |         id: meta
 66 |         uses: docker/metadata-action@v5
 67 |         with:
 68 |           images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
 69 |           tags: |
 70 |             type=raw,value=latest,enable={{is_default_branch}}
 71 |             type=ref,event=branch
 72 |             type=ref,event=pr
 73 |             type=semver,pattern={{version}}
 74 |             type=semver,pattern={{major}}.{{minor}}
 75 |             type=semver,pattern={{major}}
 76 |             type=sha,format=long
 77 | 
 78 |       - name: Build and push Docker image
 79 |         id: build
 80 |         uses: docker/build-push-action@v5
 81 |         with:
 82 |           context: .
 83 |           push: ${{ github.event_name != 'pull_request' }}
 84 |           tags: ${{ steps.meta.outputs.tags }}
 85 |           labels: ${{ steps.meta.outputs.labels }}
 86 |           cache-from: type=gha
 87 |           cache-to: type=gha,mode=max
 88 |           platforms: linux/amd64,linux/arm64
 89 |           build-args: |
 90 |             DOCKER_HASH=${{ github.sha }}
 91 | 
 92 |   cleanup:
 93 |     name: Cleanup
 94 |     needs: build-and-deploy
 95 |     runs-on: ubuntu-latest
 96 |     
 97 |     steps:
 98 |       - name: Remove old packages
 99 |         uses: actions/delete-package-versions@v4
100 |         with:
101 |           package-name: ${{ github.event.repository.name }}
102 |           package-type: container
103 |           min-versions-to-keep: 10
104 |           delete-only-untagged-versions: true
105 | 
```

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

```typescript
 1 | import { registerGmailScopes, GMAIL_SCOPES } from '../scopes.js';
 2 | import { scopeRegistry } from '../../tools/scope-registry.js';
 3 | 
 4 | describe('Gmail Scopes', () => {
 5 |   beforeEach(() => {
 6 |     // Reset the scope registry before each test
 7 |     // @ts-expect-error - accessing private property for testing
 8 |     scopeRegistry.scopes = new Map();
 9 |     // @ts-expect-error - accessing private property for testing
10 |     scopeRegistry.scopeOrder = [];
11 |   });
12 | 
13 |   it('should register all required Gmail scopes', () => {
14 |     registerGmailScopes();
15 |     const registeredScopes = scopeRegistry.getAllScopes();
16 |     
17 |     // Required scopes for Gmail functionality
18 |     const requiredScopes = [
19 |       GMAIL_SCOPES.READONLY,  // For reading emails and labels
20 |       GMAIL_SCOPES.SEND,      // For sending emails
21 |       GMAIL_SCOPES.MODIFY,    // For modifying emails and drafts
22 |       GMAIL_SCOPES.LABELS,    // For label management
23 |       GMAIL_SCOPES.SETTINGS_BASIC,    // For Gmail settings
24 |       GMAIL_SCOPES.SETTINGS_SHARING   // For settings management
25 |     ];
26 | 
27 |     // Verify each required scope is registered
28 |     requiredScopes.forEach(scope => {
29 |       expect(registeredScopes).toContain(scope);
30 |     });
31 |   });
32 | 
33 |   it('should register scopes in correct order', () => {
34 |     registerGmailScopes();
35 |     const registeredScopes = scopeRegistry.getAllScopes();
36 | 
37 |     // Core functionality scopes should come first
38 |     const coreScopes = [
39 |       GMAIL_SCOPES.READONLY,
40 |       GMAIL_SCOPES.SEND,
41 |       GMAIL_SCOPES.MODIFY
42 |     ];
43 | 
44 |     // Feature-specific scopes should come next
45 |     const featureScopes = [
46 |       GMAIL_SCOPES.LABELS
47 |     ];
48 | 
49 |     // Settings scopes should come last
50 |     const settingsScopes = [
51 |       GMAIL_SCOPES.SETTINGS_BASIC,
52 |       GMAIL_SCOPES.SETTINGS_SHARING
53 |     ];
54 | 
55 |     // Verify order of scope groups
56 |     const firstCoreIndex = Math.min(...coreScopes.map(scope => registeredScopes.indexOf(scope)));
57 |     const firstFeatureIndex = Math.min(...featureScopes.map(scope => registeredScopes.indexOf(scope)));
58 |     const firstSettingsIndex = Math.min(...settingsScopes.map(scope => registeredScopes.indexOf(scope)));
59 | 
60 |     expect(firstCoreIndex).toBeLessThan(firstFeatureIndex);
61 |     expect(firstFeatureIndex).toBeLessThan(firstSettingsIndex);
62 |   });
63 | 
64 |   it('should maintain scope registration when re-registering', () => {
65 |     // Register scopes first time
66 |     registerGmailScopes();
67 |     const initialScopes = scopeRegistry.getAllScopes();
68 | 
69 |     // Register scopes second time
70 |     registerGmailScopes();
71 |     const finalScopes = scopeRegistry.getAllScopes();
72 | 
73 |     // Verify all scopes are still registered in same order
74 |     expect(finalScopes).toEqual(initialScopes);
75 |   });
76 | });
77 | 
```

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

```typescript
 1 | import { GoogleApiRequestParams, GoogleApiResponse, GoogleApiError } from '../types.js';
 2 | import { GoogleApiRequest } from './request.js';
 3 | import { EndpointValidator } from './validators/endpoint.js';
 4 | import { ParameterValidator } from './validators/parameter.js';
 5 | import { AttachmentResponseTransformer } from '../modules/attachments/response-transformer.js';
 6 | import { AttachmentIndexService } from '../modules/attachments/index-service.js';
 7 | 
 8 | export class RequestHandler {
 9 |   private endpointValidator: EndpointValidator;
10 |   private parameterValidator: ParameterValidator;
11 |   private attachmentTransformer: AttachmentResponseTransformer;
12 | 
13 |   constructor(
14 |     private apiRequest: GoogleApiRequest,
15 |     attachmentIndexService: AttachmentIndexService = AttachmentIndexService.getInstance()
16 |   ) {
17 |     this.endpointValidator = new EndpointValidator();
18 |     this.parameterValidator = new ParameterValidator();
19 |     this.attachmentTransformer = new AttachmentResponseTransformer(attachmentIndexService);
20 |   }
21 | 
22 |   async handleRequest(params: GoogleApiRequestParams, token: string): Promise<GoogleApiResponse> {
23 |     try {
24 |       // Step 1: Basic validation
25 |       await this.validateRequest(params);
26 | 
27 |       // Step 2: Execute request
28 |       const result = await this.executeRequest(params, token);
29 | 
30 |       // Step 3: Format response
31 |       return this.formatSuccessResponse(result);
32 |     } catch (error) {
33 |       return this.formatErrorResponse(error);
34 |     }
35 |   }
36 | 
37 |   private async validateRequest(params: GoogleApiRequestParams): Promise<void> {
38 |     // Validate endpoint format and availability
39 |     await this.endpointValidator.validate(params.api_endpoint);
40 | 
41 |     // Validate required parameters
42 |     await this.parameterValidator.validate(params);
43 |   }
44 | 
45 |   private async executeRequest(params: GoogleApiRequestParams, token: string): Promise<any> {
46 |     return this.apiRequest.makeRequest({
47 |       endpoint: params.api_endpoint,
48 |       method: params.method,
49 |       params: params.params,
50 |       token
51 |     });
52 |   }
53 | 
54 |   private formatSuccessResponse(data: any): GoogleApiResponse {
55 |     // Transform response to simplify attachments for AI
56 |     const transformedData = this.attachmentTransformer.transformResponse(data);
57 |     
58 |     return {
59 |       status: 'success',
60 |       data: transformedData
61 |     };
62 |   }
63 | 
64 |   private formatErrorResponse(error: unknown): GoogleApiResponse {
65 |     if (error instanceof GoogleApiError) {
66 |       return {
67 |         status: 'error',
68 |         error: error.message,
69 |         resolution: error.resolution
70 |       };
71 |     }
72 | 
73 |     // Handle unexpected errors
74 |     return {
75 |       status: 'error',
76 |       error: error instanceof Error ? error.message : 'Unknown error occurred',
77 |       resolution: 'Please try again or contact support if the issue persists'
78 |     };
79 |   }
80 | }
81 | 
```

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

```typescript
 1 | import {
 2 |   GetContactsParams,
 3 |   GetContactsResponse,
 4 |   ContactsError
 5 | } from "../modules/contacts/types.js";
 6 | import { ContactsService } from "../services/contacts/index.js";
 7 | import { validateEmail } from "../utils/account.js";
 8 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
 9 | import { getAccountManager } from "../modules/accounts/index.js";
10 | 
11 | // Singleton instances - Initialize or inject as per project pattern
12 | let contactsService: ContactsService;
13 | let accountManager: ReturnType<typeof getAccountManager>;
14 | 
15 | /**
16 |  * Initialize required services.
17 |  * This should likely be integrated into a central initialization process.
18 |  */
19 | async function initializeServices() {
20 |   if (!contactsService) {
21 |     // Assuming ContactsService has a static getInstance or similar
22 |     // or needs to be instantiated here. Using direct instantiation for now.
23 |     contactsService = new ContactsService();
24 |     // If ContactsService requires async initialization await it here.
25 |     // await contactsService.initialize();
26 |   }
27 | 
28 |   if (!accountManager) {
29 |     accountManager = getAccountManager();
30 |   }
31 | }
32 | 
33 | /**
34 |  * Handler function for retrieving Google Contacts.
35 |  */
36 | export async function handleGetContacts(
37 |   params: GetContactsParams
38 | ): Promise<GetContactsResponse> {
39 |   await initializeServices(); // Ensure services are ready
40 |   const { email, personFields, pageSize, pageToken } = params;
41 | 
42 |   if (!email) {
43 |     throw new McpError(ErrorCode.InvalidParams, "Email address is required");
44 |   }
45 |   validateEmail(email);
46 | 
47 |   if (!personFields) {
48 |     throw new McpError(
49 |       ErrorCode.InvalidParams,
50 |       'personFields parameter is required (e.g. "names,emailAddresses")'
51 |     );
52 |   }
53 | 
54 |   // Use accountManager for token renewal like in Gmail handlers
55 |   return accountManager.withTokenRenewal(email, async () => {
56 |     try {
57 |       const result = await contactsService.getContacts({
58 |         email,
59 |         personFields,
60 |         pageSize,
61 |         pageToken
62 |       });
63 |       return result;
64 |     } catch (error) {
65 |       if (error instanceof ContactsError) {
66 |         // Map ContactsError to McpError
67 |         throw new McpError(
68 |           ErrorCode.InternalError, // Or map specific error codes
69 |           `Contacts API Error: ${error.message}`,
70 |           { code: error.code, details: error.details }
71 |         );
72 |       } else if (error instanceof McpError) {
73 |         // Re-throw existing McpErrors (like auth errors from token renewal)
74 |         throw error;
75 |       } else {
76 |         // Catch unexpected errors
77 |         throw new McpError(
78 |           ErrorCode.InternalError,
79 |           `Failed to get contacts: ${
80 |             error instanceof Error ? error.message : "Unknown error"
81 |           }`
82 |         );
83 |       }
84 |     }
85 |   });
86 | }
87 | 
88 | // Add other handlers like handleSearchContacts later
89 | 
```

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

```typescript
 1 | import { scopeRegistry } from '../scope-registry.js';
 2 | 
 3 | describe('ScopeRegistry', () => {
 4 |   beforeEach(() => {
 5 |     // Reset the scope registry before each test
 6 |     // @ts-expect-error - accessing private property for testing
 7 |     scopeRegistry.scopes = new Map();
 8 |     // @ts-expect-error - accessing private property for testing
 9 |     scopeRegistry.scopeOrder = [];
10 |   });
11 | 
12 |   it('should maintain scope registration order', () => {
13 |     const scopes = [
14 |       'https://www.googleapis.com/auth/gmail.readonly',
15 |       'https://www.googleapis.com/auth/gmail.send',
16 |       'https://www.googleapis.com/auth/gmail.modify',
17 |       'https://www.googleapis.com/auth/gmail.labels'
18 |     ];
19 | 
20 |     // Register scopes
21 |     scopes.forEach(scope => {
22 |       scopeRegistry.registerScope('gmail', scope);
23 |     });
24 | 
25 |     // Verify order matches registration order
26 |     expect(scopeRegistry.getAllScopes()).toEqual(scopes);
27 |   });
28 | 
29 |   it('should update scope position when re-registered', () => {
30 |     const scope1 = 'https://www.googleapis.com/auth/gmail.readonly';
31 |     const scope2 = 'https://www.googleapis.com/auth/gmail.send';
32 |     const scope3 = 'https://www.googleapis.com/auth/gmail.labels';
33 | 
34 |     // Register initial scopes
35 |     scopeRegistry.registerScope('gmail', scope1);
36 |     scopeRegistry.registerScope('gmail', scope2);
37 |     scopeRegistry.registerScope('gmail', scope3);
38 | 
39 |     // Re-register scope1 (should move to end)
40 |     scopeRegistry.registerScope('gmail', scope1);
41 | 
42 |     // Verify new order
43 |     expect(scopeRegistry.getAllScopes()).toEqual([
44 |       scope2,
45 |       scope3,
46 |       scope1
47 |     ]);
48 |   });
49 | 
50 |   it('should maintain tool associations when re-registering scopes', () => {
51 |     const scope = 'https://www.googleapis.com/auth/gmail.labels';
52 |     
53 |     // Register with first tool
54 |     scopeRegistry.registerScope('tool1', scope);
55 |     
56 |     // Re-register with second tool
57 |     scopeRegistry.registerScope('tool2', scope);
58 |     
59 |     // Get scopes for both tools
60 |     const tool1Scopes = scopeRegistry.getToolScopes('tool1');
61 |     const tool2Scopes = scopeRegistry.getToolScopes('tool2');
62 |     
63 |     // Verify scope is associated with latest tool only
64 |     expect(tool1Scopes).not.toContain(scope);
65 |     expect(tool2Scopes).toContain(scope);
66 |   });
67 | 
68 |   it('should return empty array for non-existent tool', () => {
69 |     const scopes = scopeRegistry.getToolScopes('non-existent-tool');
70 |     expect(scopes).toEqual([]);
71 |   });
72 | 
73 |   it('should handle multiple scopes for same tool', () => {
74 |     const tool = 'gmail';
75 |     const scopes = [
76 |       'https://www.googleapis.com/auth/gmail.readonly',
77 |       'https://www.googleapis.com/auth/gmail.send',
78 |       'https://www.googleapis.com/auth/gmail.labels'
79 |     ];
80 | 
81 |     scopes.forEach(scope => {
82 |       scopeRegistry.registerScope(tool, scope);
83 |     });
84 | 
85 |     const toolScopes = scopeRegistry.getToolScopes(tool);
86 |     expect(toolScopes).toEqual(scopes);
87 |   });
88 | });
89 | 
```

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

```typescript
 1 | import { SearchCriteria } from '../types.js';
 2 | 
 3 | export class SearchService {
 4 |   /**
 5 |    * Builds a Gmail search query string from SearchCriteria
 6 |    */
 7 |   buildSearchQuery(criteria: SearchCriteria = {}): string {
 8 |     const queryParts: string[] = [];
 9 | 
10 |     // Handle from (support multiple senders)
11 |     if (criteria.from) {
12 |       const fromAddresses = Array.isArray(criteria.from) ? criteria.from : [criteria.from];
13 |       if (fromAddresses.length === 1) {
14 |         queryParts.push(`from:${fromAddresses[0]}`);
15 |       } else {
16 |         queryParts.push(`{${fromAddresses.map(f => `from:${f}`).join(' OR ')}}`);
17 |       }
18 |     }
19 | 
20 |     // Handle to (support multiple recipients)
21 |     if (criteria.to) {
22 |       const toAddresses = Array.isArray(criteria.to) ? criteria.to : [criteria.to];
23 |       if (toAddresses.length === 1) {
24 |         queryParts.push(`to:${toAddresses[0]}`);
25 |       } else {
26 |         queryParts.push(`{${toAddresses.map(t => `to:${t}`).join(' OR ')}}`);
27 |       }
28 |     }
29 | 
30 |     // Handle subject (escape special characters and quotes)
31 |     if (criteria.subject) {
32 |       const escapedSubject = criteria.subject.replace(/["\\]/g, '\\$&');
33 |       queryParts.push(`subject:"${escapedSubject}"`);
34 |     }
35 | 
36 |     // Handle content (escape special characters and quotes)
37 |     if (criteria.content) {
38 |       const escapedContent = criteria.content.replace(/["\\]/g, '\\$&');
39 |       queryParts.push(`"${escapedContent}"`);
40 |     }
41 | 
42 |     // Handle date range (use Gmail's date format: YYYY/MM/DD)
43 |     if (criteria.after) {
44 |       const afterDate = new Date(criteria.after);
45 |       const afterStr = `${afterDate.getFullYear()}/${(afterDate.getMonth() + 1).toString().padStart(2, '0')}/${afterDate.getDate().toString().padStart(2, '0')}`;
46 |       queryParts.push(`after:${afterStr}`);
47 |     }
48 |     if (criteria.before) {
49 |       const beforeDate = new Date(criteria.before);
50 |       const beforeStr = `${beforeDate.getFullYear()}/${(beforeDate.getMonth() + 1).toString().padStart(2, '0')}/${beforeDate.getDate().toString().padStart(2, '0')}`;
51 |       queryParts.push(`before:${beforeStr}`);
52 |     }
53 | 
54 |     // Handle attachments
55 |     if (criteria.hasAttachment) {
56 |       queryParts.push('has:attachment');
57 |     }
58 | 
59 |     // Handle labels (no need to join with spaces, Gmail supports multiple label: operators)
60 |     if (criteria.labels && criteria.labels.length > 0) {
61 |       criteria.labels.forEach(label => {
62 |         queryParts.push(`label:${label}`);
63 |       });
64 |     }
65 | 
66 |     // Handle excluded labels
67 |     if (criteria.excludeLabels && criteria.excludeLabels.length > 0) {
68 |       criteria.excludeLabels.forEach(label => {
69 |         queryParts.push(`-label:${label}`);
70 |       });
71 |     }
72 | 
73 |     // Handle spam/trash inclusion
74 |     if (criteria.includeSpam) {
75 |       queryParts.push('in:anywhere');
76 |     }
77 | 
78 |     // Handle read/unread status
79 |     if (criteria.isUnread !== undefined) {
80 |       queryParts.push(criteria.isUnread ? 'is:unread' : 'is:read');
81 |     }
82 | 
83 |     return queryParts.join(' ');
84 |   }
85 | }
86 | 
```

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

```markdown
  1 | # Attachment System Refactor
  2 | 
  3 | ## Completed
  4 | - Created AttachmentIndexService
  5 |   - Map-based storage using messageId + filename as key
  6 |   - Built-in size limit (256 entries)
  7 |   - Expiry handling (1 hour timeout)
  8 | 
  9 | - Created AttachmentResponseTransformer
 10 |   - Simplifies attachment info for AI (filename only)
 11 |   - Integrated into API layer
 12 | 
 13 | - Updated Services
 14 |   - Gmail attachment handling now uses simplified format
 15 |   - Calendar attachment handling now uses simplified format
 16 | 
 17 | - Added Cleanup System
 18 |   - Created AttachmentCleanupService
 19 |   - Cleanup triggers on:
 20 |     - Index reaching size limit
 21 |     - Retrieving expired attachments
 22 | 
 23 | ## Implementation Status
 24 | 
 25 | ### Completed ✓
 26 | 1. Core Components
 27 |    - AttachmentIndexService with map-based storage
 28 |    - Size limit (256 entries) implementation
 29 |    - Expiry handling (1 hour timeout)
 30 |    - Filename + messageId based lookup
 31 | 
 32 | 2. Response Transformation
 33 |    - AttachmentResponseTransformer implementation
 34 |    - Unified handling for email and calendar attachments
 35 |    - Simplified format for AI (filename only)
 36 |    - Full metadata preservation internally
 37 | 
 38 | 3. Service Integration
 39 |    - Gmail attachment handling
 40 |    - Calendar attachment handling
 41 |    - Abstracted attachment interface
 42 | 
 43 | 4. Test Infrastructure
 44 |    - Basic test suite setup
 45 |    - Core functionality tests
 46 |    - Integration test structure
 47 | 
 48 | ### Completed ✓
 49 | 1. Testing Fixes
 50 |    - ✓ Simplified test suite to focus on core functionality
 51 |    - ✓ Removed complex timing-dependent tests
 52 |    - ✓ Added basic service operation tests
 53 |    - ✓ Verified cleanup service functionality
 54 |    - ✓ Fixed Drive service test timing issues
 55 | 
 56 | 2. Cleanup System Refinements
 57 |    - ✓ Immediate cleanup on service start
 58 |    - ✓ Activity-based interval adjustments
 59 |    - ✓ Performance monitoring accuracy
 60 | 
 61 | ### Version 1.1 Changes ✓
 62 | 1. Attachment System Improvements
 63 |    - ✓ Simplified attachment data in responses (filename only)
 64 |    - ✓ Maintained full metadata in index service
 65 |    - ✓ Verified download functionality with simplified format
 66 |    - ✓ Updated documentation and architecture
 67 | 
 68 | ### Next Steps 📋
 69 | 1. Documentation
 70 |    - [x] Add inline documentation
 71 |    - [x] Update API documentation
 72 |    - [x] Add usage examples
 73 | 
 74 | ## Example Transformation
 75 | Before:
 76 | ```json
 77 | {
 78 |   "id": "1952a804b3a15f6a",
 79 |   "attachments": [{
 80 |     "id": "ANGjdJ9gKpYkZ5NRp80mRJVCUe9XsAB93LHl22UrPU-9-pBPadGczuK3...",
 81 |     "name": "document.pdf",
 82 |     "mimeType": "application/pdf",
 83 |     "size": 1234
 84 |   }]
 85 | }
 86 | ```
 87 | 
 88 | After:
 89 | ```json
 90 | {
 91 |   "id": "1952a804b3a15f6a",
 92 |   "attachments": [{
 93 |     "name": "document.pdf"
 94 |   }]
 95 | }
 96 | 
 97 | ### Future Improvements 🚀
 98 | 1. Performance Optimizations
 99 |    - [ ] Implement batch processing for large attachment sets
100 |    - [ ] Add caching layer for frequently accessed attachments
101 |    - [ ] Optimize cleanup intervals based on usage patterns
102 | 
103 | 2. Enhanced Features
104 |    - [ ] Support for streaming large attachments
105 |    - [ ] Add compression options for storage
106 |    - [ ] Implement selective metadata retention
107 | 
```

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

```typescript
  1 | #!/usr/bin/env node
  2 | import fs from 'fs/promises';
  3 | import path from 'path';
  4 | import { fileURLToPath } from 'url';
  5 | 
  6 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
  7 | const ROOT_DIR = path.resolve(__dirname, '../..');
  8 | 
  9 | interface SetupCheck {
 10 |   path: string;
 11 |   type: 'file' | 'directory';
 12 |   example?: string;
 13 |   required: boolean;
 14 | }
 15 | 
 16 | const REQUIRED_ITEMS: SetupCheck[] = [
 17 |   {
 18 |     path: 'config',
 19 |     type: 'directory',
 20 |     required: true
 21 |   },
 22 |   {
 23 |     path: 'config/credentials',
 24 |     type: 'directory',
 25 |     required: true
 26 |   },
 27 |   {
 28 |     path: 'config/gauth.json',
 29 |     type: 'file',
 30 |     example: 'config/gauth.example.json',
 31 |     required: true
 32 |   },
 33 |   {
 34 |     path: 'config/accounts.json',
 35 |     type: 'file',
 36 |     example: 'config/accounts.example.json',
 37 |     required: true
 38 |   }
 39 | ];
 40 | 
 41 | async function fileExists(path: string): Promise<boolean> {
 42 |   try {
 43 |     await fs.access(path);
 44 |     return true;
 45 |   } catch {
 46 |     return false;
 47 |   }
 48 | }
 49 | 
 50 | async function setupEnvironment(): Promise<void> {
 51 |   console.log('\n🔧 Setting up Google Workspace MCP Server environment...\n');
 52 |   
 53 |   for (const item of REQUIRED_ITEMS) {
 54 |     const fullPath = path.join(ROOT_DIR, item.path);
 55 |     const exists = await fileExists(fullPath);
 56 |     
 57 |     if (!exists) {
 58 |       if (item.type === 'directory') {
 59 |         console.log(`📁 Creating directory: ${item.path}`);
 60 |         await fs.mkdir(fullPath, { recursive: true });
 61 |       } else if (item.example) {
 62 |         const examplePath = path.join(ROOT_DIR, item.example);
 63 |         const exampleExists = await fileExists(examplePath);
 64 |         
 65 |         if (exampleExists) {
 66 |           console.log(`📄 Creating ${item.path} from example`);
 67 |           await fs.copyFile(examplePath, fullPath);
 68 |         } else if (item.required) {
 69 |           console.error(`❌ Error: Example file ${item.example} not found`);
 70 |           process.exit(1);
 71 |         }
 72 |       } else if (item.required) {
 73 |         console.error(`❌ Error: Required ${item.type} ${item.path} is missing`);
 74 |         process.exit(1);
 75 |       }
 76 |     } else {
 77 |       console.log(`✅ ${item.path} already exists`);
 78 |     }
 79 |   }
 80 | 
 81 |   console.log('\n✨ Environment setup complete!\n');
 82 |   console.log('Next steps:');
 83 |   console.log('1. Configure OAuth credentials in config/gauth.json');
 84 |   console.log('   - Create a project in Google Cloud Console');
 85 |   console.log('   - Enable Gmail API');
 86 |   console.log('   - Configure OAuth consent screen');
 87 |   console.log('   - Create OAuth 2.0 credentials');
 88 |   console.log('   - Copy credentials to config/gauth.json');
 89 |   console.log('\n2. Configure accounts in config/accounts.json');
 90 |   console.log('   - Add Google accounts you want to use');
 91 |   console.log('   - Set appropriate categories and descriptions');
 92 |   console.log('\n3. Run authentication for each account:');
 93 |   console.log('   ```');
 94 |   console.log('   npx ts-node src/scripts/setup-google-env.ts');
 95 |   console.log('   ```');
 96 | }
 97 | 
 98 | setupEnvironment().catch(error => {
 99 |   console.error('\n❌ Setup failed:', error);
100 |   process.exit(1);
101 | });
102 | 
```

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

```typescript
  1 | /**
  2 |  * Service for managing attachment metadata and providing simplified references for AI interactions
  3 |  */
  4 | 
  5 | export interface AttachmentMetadataInternal {
  6 |   messageId: string;
  7 |   filename: string;
  8 |   originalId: string;
  9 |   mimeType: string;
 10 |   size: number;
 11 |   timestamp: number;
 12 | }
 13 | 
 14 | export class AttachmentIndexService {
 15 |   private static instance: AttachmentIndexService;
 16 |   private index: Map<string, AttachmentMetadataInternal>;
 17 |   private readonly _maxEntries: number = 256;
 18 |   private expiryMs: number = 3600000; // 1 hour
 19 | 
 20 |   /**
 21 |    * Get the maximum number of entries allowed in the index
 22 |    */
 23 |   get maxEntries(): number {
 24 |     return this._maxEntries;
 25 |   }
 26 | 
 27 |   private constructor() {
 28 |     this.index = new Map();
 29 |   }
 30 | 
 31 |   /**
 32 |    * Get the singleton instance
 33 |    */
 34 |   public static getInstance(): AttachmentIndexService {
 35 |     if (!AttachmentIndexService.instance) {
 36 |       AttachmentIndexService.instance = new AttachmentIndexService();
 37 |     }
 38 |     return AttachmentIndexService.instance;
 39 |   }
 40 | 
 41 |   /**
 42 |    * Add or update attachment metadata in the index
 43 |    */
 44 |   addAttachment(messageId: string, attachment: {
 45 |     id: string;
 46 |     name: string;
 47 |     mimeType: string;
 48 |     size: number;
 49 |   }): void {
 50 |     // Clean expired entries if we're near capacity
 51 |     if (this.index.size >= this._maxEntries) {
 52 |       this.cleanExpiredEntries();
 53 |       
 54 |       // If still at capacity after cleaning expired entries,
 55 |       // remove oldest entries until we have space
 56 |       if (this.index.size >= this._maxEntries) {
 57 |         const entries = Array.from(this.index.entries())
 58 |           .sort(([, a], [, b]) => a.timestamp - b.timestamp);
 59 |           
 60 |         while (this.index.size >= this._maxEntries && entries.length > 0) {
 61 |           const [key] = entries.shift()!;
 62 |           this.index.delete(key);
 63 |         }
 64 |       }
 65 |     }
 66 | 
 67 |     const key = `${messageId}_${attachment.name}`;
 68 |     this.index.set(key, {
 69 |       messageId,
 70 |       filename: attachment.name,
 71 |       originalId: attachment.id,
 72 |       mimeType: attachment.mimeType,
 73 |       size: attachment.size,
 74 |       timestamp: Date.now()
 75 |     });
 76 |   }
 77 | 
 78 |   /**
 79 |    * Retrieve attachment metadata by message ID and filename
 80 |    */
 81 |   getMetadata(messageId: string, filename: string): AttachmentMetadataInternal | undefined {
 82 |     const key = `${messageId}_${filename}`;
 83 |     const metadata = this.index.get(key);
 84 | 
 85 |     if (metadata && this.isExpired(metadata)) {
 86 |       this.index.delete(key);
 87 |       return undefined;
 88 |     }
 89 | 
 90 |     return metadata;
 91 |   }
 92 | 
 93 |   /**
 94 |    * Remove expired entries from the index
 95 |    */
 96 |   public cleanExpiredEntries(): void {
 97 |     const now = Date.now();
 98 |     for (const [key, metadata] of this.index.entries()) {
 99 |       if (this.isExpired(metadata)) {
100 |         this.index.delete(key);
101 |       }
102 |     }
103 |   }
104 | 
105 |   /**
106 |    * Check if an entry has expired
107 |    */
108 |   private isExpired(metadata: AttachmentMetadataInternal): boolean {
109 |     return Date.now() - metadata.timestamp > this.expiryMs;
110 |   }
111 | 
112 |   /**
113 |    * Get current number of entries in the index
114 |    */
115 |   get size(): number {
116 |     return this.index.size;
117 |   }
118 | }
119 | 
```

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

```typescript
 1 | #!/usr/bin/env node
 2 | import fs from 'fs/promises';
 3 | import path from 'path';
 4 | import { fileURLToPath } from 'url';
 5 | 
 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
 7 | const ROOT_DIR = path.resolve(__dirname, '../..');
 8 | 
 9 | async function setupGoogleEnv(): Promise<void> {
10 |   try {
11 |     // Ensure credentials directory exists
12 |     const credentialsDir = path.join(ROOT_DIR, 'config', 'credentials');
13 |     try {
14 |       await fs.access(credentialsDir);
15 |     } catch {
16 |       await fs.mkdir(credentialsDir, { recursive: true });
17 |     }
18 | 
19 |     // Read and encode the credentials file
20 |     const gauthPath = path.join(ROOT_DIR, 'config', 'gauth.json');
21 |     const credentials = await fs.readFile(gauthPath, 'utf8');
22 |     const credentialsBase64 = Buffer.from(credentials).toString('base64');
23 | 
24 |     // Read and encode any existing tokens
25 |     const tokenFiles = await fs.readdir(credentialsDir);
26 |     const tokenData: Record<string, string> = {};
27 | 
28 |     for (const file of tokenFiles) {
29 |       if (file.endsWith('.token.json')) {
30 |         const email = file.replace('.token.json', '').replace(/-/g, '.');
31 |         const tokenPath = path.join(credentialsDir, file);
32 |         const token = await fs.readFile(tokenPath, 'utf8');
33 |         tokenData[email] = Buffer.from(token).toString('base64');
34 |       }
35 |     }
36 | 
37 |     // Read existing .env if it exists
38 |     const envPath = path.join(ROOT_DIR, '.env');
39 |     let envContent = '';
40 |     try {
41 |       envContent = await fs.readFile(envPath, 'utf8');
42 |       // Remove any existing Google credentials
43 |       envContent = envContent
44 |         .split('\n')
45 |         .filter(line => !line.startsWith('GOOGLE_'))
46 |         .join('\n');
47 |       if (envContent && !envContent.endsWith('\n')) {
48 |         envContent += '\n';
49 |       }
50 |     } catch {
51 |       // If .env doesn't exist, start with empty content
52 |     }
53 | 
54 |     // Add the new credentials
55 |     envContent += `GOOGLE_CREDENTIALS=${credentialsBase64}\n`;
56 |     
57 |     // Add tokens for each account
58 |     for (const [email, token] of Object.entries(tokenData)) {
59 |       const safeEmail = email.replace(/[@.]/g, '_').toUpperCase();
60 |       envContent += `GOOGLE_TOKEN_${safeEmail}=${token}\n`;
61 |     }
62 | 
63 |     // Write to .env file
64 |     await fs.writeFile(envPath, envContent);
65 | 
66 |     console.log('\n✅ Successfully configured Google environment:');
67 |     console.log(`- Credentials loaded from: ${gauthPath}`);
68 |     console.log(`- Tokens loaded: ${Object.keys(tokenData).length}`);
69 |     console.log(`- Environment variables written to: ${envPath}`);
70 |     
71 |     if (Object.keys(tokenData).length === 0) {
72 |       console.log('\nℹ️  No tokens found. Run authentication for each account to generate tokens.');
73 |     }
74 | 
75 |   } catch (error) {
76 |     console.error('\n❌ Setup failed:', error instanceof Error ? error.message : error);
77 |     console.log('\nPlease ensure:');
78 |     console.log('1. config/gauth.json exists with valid Google OAuth credentials');
79 |     console.log('2. You have write permissions for the .env file');
80 |     process.exit(1);
81 |   }
82 | }
83 | 
84 | setupGoogleEnv().catch(error => {
85 |   console.error('Fatal error:', error);
86 |   process.exit(1);
87 | });
88 | 
```

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

```typescript
  1 | import path from 'path';
  2 | import fs from 'fs/promises';
  3 | 
  4 | export class WorkspaceManager {
  5 |   private basePath: string;
  6 | 
  7 |   constructor() {
  8 |     this.basePath = process.env.WORKSPACE_BASE_PATH || '/app/workspace';
  9 |   }
 10 | 
 11 |   /**
 12 |    * Get the workspace directory path for a specific email account
 13 |    */
 14 |   private getAccountPath(email: string): string {
 15 |     return path.join(this.basePath, email);
 16 |   }
 17 | 
 18 |   /**
 19 |    * Get the downloads directory path for a specific email account
 20 |    */
 21 |   private getDownloadsPath(email: string): string {
 22 |     return path.join(this.getAccountPath(email), 'downloads');
 23 |   }
 24 | 
 25 |   /**
 26 |    * Get the uploads directory path for a specific email account
 27 |    */
 28 |   private getUploadsPath(email: string): string {
 29 |     return path.join(this.getAccountPath(email), 'uploads');
 30 |   }
 31 | 
 32 |   /**
 33 |    * Get the shared temporary directory path
 34 |    */
 35 |   private getTempPath(): string {
 36 |     return path.join(this.basePath, 'shared', 'temp');
 37 |   }
 38 | 
 39 |   /**
 40 |    * Ensure all required directories exist for an email account
 41 |    */
 42 |   async initializeAccountDirectories(email: string): Promise<void> {
 43 |     const dirs = [
 44 |       this.getAccountPath(email),
 45 |       this.getDownloadsPath(email),
 46 |       this.getUploadsPath(email)
 47 |     ];
 48 | 
 49 |     for (const dir of dirs) {
 50 |       await fs.mkdir(dir, { recursive: true, mode: 0o750 });
 51 |     }
 52 |   }
 53 | 
 54 |   /**
 55 |    * Generate a path for a downloaded file
 56 |    */
 57 |   async getDownloadPath(email: string, filename: string): Promise<string> {
 58 |     await this.initializeAccountDirectories(email);
 59 |     return path.join(this.getDownloadsPath(email), filename);
 60 |   }
 61 | 
 62 |   /**
 63 |    * Generate a path for an upload file
 64 |    */
 65 |   async getUploadPath(email: string, filename: string): Promise<string> {
 66 |     await this.initializeAccountDirectories(email);
 67 |     return path.join(this.getUploadsPath(email), filename);
 68 |   }
 69 | 
 70 |   /**
 71 |    * Generate a temporary file path
 72 |    */
 73 |   async getTempFilePath(prefix: string): Promise<string> {
 74 |     const tempDir = this.getTempPath();
 75 |     await fs.mkdir(tempDir, { recursive: true, mode: 0o750 });
 76 |     const timestamp = new Date().getTime();
 77 |     const random = Math.random().toString(36).substring(2, 15);
 78 |     return path.join(tempDir, `${prefix}-${timestamp}-${random}`);
 79 |   }
 80 | 
 81 |   /**
 82 |    * Clean up old files in the temporary directory
 83 |    * @param maxAge Maximum age in milliseconds before a file is considered old
 84 |    */
 85 |   async cleanupTempFiles(maxAge: number = 24 * 60 * 60 * 1000): Promise<void> {
 86 |     const tempDir = this.getTempPath();
 87 |     try {
 88 |       const files = await fs.readdir(tempDir);
 89 |       const now = Date.now();
 90 | 
 91 |       for (const file of files) {
 92 |         const filePath = path.join(tempDir, file);
 93 |         const stats = await fs.stat(filePath);
 94 | 
 95 |         if (now - stats.mtimeMs > maxAge) {
 96 |           await fs.unlink(filePath);
 97 |         }
 98 |       }
 99 |     } catch (error: unknown) {
100 |       // Ignore errors if temp directory doesn't exist
101 |       if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
102 |         throw error;
103 |       }
104 |     }
105 |   }
106 | }
107 | 
108 | // Export singleton instance
109 | export const workspaceManager = new WorkspaceManager();
110 | 
```

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

```typescript
 1 | import { OAuth2Client } from 'google-auth-library';
 2 | import { AccountError } from './types.js';
 3 | import { OAuthCallbackServer } from './callback-server.js';
 4 | import logger from '../../utils/logger.js';
 5 | 
 6 | export class GoogleOAuthClient {
 7 |   private oauth2Client: OAuth2Client;
 8 |   private callbackServer: OAuthCallbackServer;
 9 | 
10 |   constructor() {
11 |     const clientId = process.env.GOOGLE_CLIENT_ID;
12 |     const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
13 |     
14 |     if (!clientId || !clientSecret) {
15 |       throw new AccountError(
16 |         'Missing OAuth credentials',
17 |         'AUTH_CONFIG_ERROR',
18 |         'GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be provided'
19 |       );
20 |     }
21 | 
22 |     this.callbackServer = OAuthCallbackServer.getInstance();
23 |     
24 |     logger.info('Initializing OAuth client...');
25 |     this.oauth2Client = new OAuth2Client(
26 |       clientId,
27 |       clientSecret,
28 |       this.callbackServer.getCallbackUrl() // Use localhost:8080 instead of OOB
29 |     );
30 |     logger.info('OAuth client initialized successfully');
31 |     
32 |     // Ensure the callback server is running
33 |     this.callbackServer.ensureServerRunning().catch(error => {
34 |       logger.error('Failed to start OAuth callback server:', error);
35 |     });
36 |   }
37 | 
38 |   getAuthClient(): OAuth2Client {
39 |     return this.oauth2Client;
40 |   }
41 | 
42 |   /**
43 |    * Generates the OAuth authorization URL
44 |    * IMPORTANT: When using the generated URL, always use it exactly as returned.
45 |    * Do not attempt to modify, reformat, or reconstruct the URL as this can break
46 |    * the authentication flow. The URL contains carefully encoded parameters that
47 |    * must be preserved exactly as provided.
48 |    */
49 |   async generateAuthUrl(scopes: string[]): Promise<string> {
50 |     logger.info('Generating OAuth authorization URL');
51 |     const url = this.oauth2Client.generateAuthUrl({
52 |       access_type: 'offline',
53 |       scope: scopes,
54 |       prompt: 'consent'
55 |     });
56 |     logger.debug('Authorization URL generated successfully');
57 |     return url;
58 |   }
59 | 
60 |   async waitForAuthorizationCode(): Promise<string> {
61 |     logger.info('Starting OAuth callback server and waiting for authorization...');
62 |     return await this.callbackServer.waitForAuthorizationCode();
63 |   }
64 | 
65 |   async getTokenFromCode(code: string): Promise<any> {
66 |     logger.info('Exchanging authorization code for tokens');
67 |     try {
68 |       const { tokens } = await this.oauth2Client.getToken(code);
69 |       logger.info('Successfully obtained tokens from auth code');
70 |       return tokens;
71 |     } catch (error) {
72 |       throw new AccountError(
73 |         'Failed to exchange authorization code for tokens',
74 |         'AUTH_CODE_ERROR',
75 |         'Please ensure the authorization code is valid and not expired'
76 |       );
77 |     }
78 |   }
79 | 
80 |   async refreshToken(refreshToken: string): Promise<any> {
81 |     logger.info('Refreshing access token');
82 |     try {
83 |       this.oauth2Client.setCredentials({
84 |         refresh_token: refreshToken
85 |       });
86 |       const { credentials } = await this.oauth2Client!.refreshAccessToken();
87 |       logger.info('Successfully refreshed access token');
88 |       return credentials;
89 |     } catch (error) {
90 |       throw new AccountError(
91 |         'Failed to refresh token',
92 |         'TOKEN_REFRESH_ERROR',
93 |         'Please re-authenticate the account'
94 |       );
95 |     }
96 |   }
97 | }
98 | 
```

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

```typescript
  1 | /**
  2 |  * Gmail's allowed label colors
  3 |  * These are the only colors that Gmail's API accepts for label customization
  4 |  */
  5 | export const GMAIL_LABEL_COLORS = {
  6 |   default: {
  7 |     textColor: '#000000',
  8 |     backgroundColor: '#ffffff'
  9 |   },
 10 |   red: {
 11 |     textColor: '#ffffff',
 12 |     backgroundColor: '#dc3545'
 13 |   },
 14 |   orange: {
 15 |     textColor: '#000000',
 16 |     backgroundColor: '#ffc107'
 17 |   },
 18 |   yellow: {
 19 |     textColor: '#000000',
 20 |     backgroundColor: '#ffeb3b'
 21 |   },
 22 |   green: {
 23 |     textColor: '#ffffff',
 24 |     backgroundColor: '#28a745'
 25 |   },
 26 |   teal: {
 27 |     textColor: '#ffffff',
 28 |     backgroundColor: '#20c997'
 29 |   },
 30 |   blue: {
 31 |     textColor: '#ffffff',
 32 |     backgroundColor: '#007bff'
 33 |   },
 34 |   purple: {
 35 |     textColor: '#ffffff',
 36 |     backgroundColor: '#6f42c1'
 37 |   },
 38 |   pink: {
 39 |     textColor: '#ffffff',
 40 |     backgroundColor: '#e83e8c'
 41 |   },
 42 |   gray: {
 43 |     textColor: '#ffffff',
 44 |     backgroundColor: '#6c757d'
 45 |   }
 46 | } as const;
 47 | 
 48 | export type GmailLabelColor = keyof typeof GMAIL_LABEL_COLORS;
 49 | 
 50 | /**
 51 |  * Validates if a color combination is allowed by Gmail
 52 |  * @param textColor - Hex color code for text
 53 |  * @param backgroundColor - Hex color code for background
 54 |  * @returns true if the color combination is valid, false otherwise
 55 |  */
 56 | export function isValidGmailLabelColor(textColor: string, backgroundColor: string): boolean {
 57 |   return Object.values(GMAIL_LABEL_COLORS).some(
 58 |     color => color.textColor.toLowerCase() === textColor.toLowerCase() &&
 59 |              color.backgroundColor.toLowerCase() === backgroundColor.toLowerCase()
 60 |   );
 61 | }
 62 | 
 63 | /**
 64 |  * Gets the closest valid Gmail label color based on a hex code
 65 |  * @param backgroundColor - Hex color code to match
 66 |  * @returns The closest matching valid Gmail label color combination
 67 |  */
 68 | export function getNearestGmailLabelColor(backgroundColor: string): typeof GMAIL_LABEL_COLORS[GmailLabelColor] {
 69 |   // Remove # if present and convert to lowercase
 70 |   const targetColor = backgroundColor.replace('#', '').toLowerCase();
 71 |   
 72 |   // Convert hex to RGB
 73 |   const targetRGB = {
 74 |     r: parseInt(targetColor.substr(0, 2), 16),
 75 |     g: parseInt(targetColor.substr(2, 2), 16),
 76 |     b: parseInt(targetColor.substr(4, 2), 16)
 77 |   };
 78 | 
 79 |   // Calculate distance to each valid color
 80 |   let closestColor = 'default' as GmailLabelColor;
 81 |   let minDistance = Number.MAX_VALUE;
 82 | 
 83 |   Object.entries(GMAIL_LABEL_COLORS).forEach(([name, color]) => {
 84 |     const validColor = color.backgroundColor.replace('#', '').toLowerCase();
 85 |     const validRGB = {
 86 |       r: parseInt(validColor.substr(0, 2), 16),
 87 |       g: parseInt(validColor.substr(2, 2), 16),
 88 |       b: parseInt(validColor.substr(4, 2), 16)
 89 |     };
 90 | 
 91 |     // Calculate Euclidean distance in RGB space
 92 |     const distance = Math.sqrt(
 93 |       Math.pow(validRGB.r - targetRGB.r, 2) +
 94 |       Math.pow(validRGB.g - targetRGB.g, 2) +
 95 |       Math.pow(validRGB.b - targetRGB.b, 2)
 96 |     );
 97 | 
 98 |     if (distance < minDistance) {
 99 |       minDistance = distance;
100 |       closestColor = name as GmailLabelColor;
101 |     }
102 |   });
103 | 
104 |   return GMAIL_LABEL_COLORS[closestColor];
105 | }
106 | 
107 | /**
108 |  * Error messages for label operations
109 |  */
110 | export const LABEL_ERROR_MESSAGES = {
111 |   INVALID_COLOR: 'Invalid label color. Please use one of the predefined Gmail label colors.',
112 |   INVALID_COLOR_COMBINATION: 'Invalid text and background color combination.',
113 |   COLOR_SUGGESTION: (original: string, suggested: typeof GMAIL_LABEL_COLORS[GmailLabelColor]) => 
114 |     `The color "${original}" is not supported. Consider using the suggested color: Background: ${suggested.backgroundColor}, Text: ${suggested.textColor}`
115 | } as const;
116 | 
```

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

```typescript
 1 | import { TokenManager } from '../../../modules/accounts/token.js';
 2 | import { GoogleOAuthClient } from '../../../modules/accounts/oauth.js';
 3 | 
 4 | jest.mock('../../../modules/accounts/oauth.js');
 5 | jest.mock('../../../utils/logger.js');
 6 | 
 7 | describe('TokenManager', () => {
 8 |   let tokenManager: TokenManager;
 9 |   let mockOAuthClient: jest.Mocked<GoogleOAuthClient>;
10 | 
11 |   beforeEach(() => {
12 |     mockOAuthClient = new GoogleOAuthClient() as jest.Mocked<GoogleOAuthClient>;
13 |     tokenManager = new TokenManager(mockOAuthClient);
14 | 
15 |     // Mock filesystem operations
16 |     jest.spyOn(tokenManager as any, 'loadToken').mockImplementation(async () => null);
17 |     jest.spyOn(tokenManager as any, 'saveToken').mockImplementation(async () => {});
18 |   });
19 | 
20 |   describe('autoRenewToken', () => {
21 |     it('should return valid status for non-expired token', async () => {
22 |       const validToken = {
23 |         access_token: 'valid_token',
24 |         refresh_token: 'refresh_token',
25 |         expiry_date: Date.now() + 3600000 // 1 hour from now
26 |       };
27 | 
28 |       (tokenManager as any).loadToken.mockResolvedValue(validToken);
29 | 
30 |       const result = await tokenManager.autoRenewToken('[email protected]');
31 |       expect(result.success).toBe(true);
32 |       expect(result.status).toBe('VALID');
33 |       expect(result.token).toBe(validToken);
34 |     });
35 | 
36 |     it('should attempt refresh for expired token', async () => {
37 |       const expiredToken = {
38 |         access_token: 'expired_token',
39 |         refresh_token: 'refresh_token',
40 |         expiry_date: Date.now() - 3600000 // 1 hour ago
41 |       };
42 | 
43 |       const newToken = {
44 |         access_token: 'new_token',
45 |         refresh_token: 'refresh_token',
46 |         expiry_date: Date.now() + 3600000
47 |       };
48 | 
49 |       (tokenManager as any).loadToken.mockResolvedValue(expiredToken);
50 |       mockOAuthClient.refreshToken.mockResolvedValue(newToken);
51 | 
52 |       const result = await tokenManager.autoRenewToken('[email protected]');
53 |       expect(result.success).toBe(true);
54 |       expect(result.status).toBe('REFRESHED');
55 |       expect(result.token).toBe(newToken);
56 |     });
57 | 
58 |     it('should handle invalid refresh token', async () => {
59 |       const expiredToken = {
60 |         access_token: 'expired_token',
61 |         refresh_token: 'invalid_refresh_token',
62 |         expiry_date: Date.now() - 3600000
63 |       };
64 | 
65 |       (tokenManager as any).loadToken.mockResolvedValue(expiredToken);
66 |       mockOAuthClient.refreshToken.mockRejectedValue(new Error('invalid_grant'));
67 | 
68 |       const result = await tokenManager.autoRenewToken('[email protected]');
69 |       expect(result.success).toBe(false);
70 |       expect(result.status).toBe('REFRESH_FAILED');
71 |       expect(result.canRetry).toBe(false);
72 |     });
73 | 
74 |     it('should handle temporary refresh failures', async () => {
75 |       const expiredToken = {
76 |         access_token: 'expired_token',
77 |         refresh_token: 'refresh_token',
78 |         expiry_date: Date.now() - 3600000
79 |       };
80 | 
81 |       (tokenManager as any).loadToken.mockResolvedValue(expiredToken);
82 |       mockOAuthClient.refreshToken.mockRejectedValue(new Error('network_error'));
83 | 
84 |       const result = await tokenManager.autoRenewToken('[email protected]');
85 |       expect(result.success).toBe(false);
86 |       expect(result.status).toBe('REFRESH_FAILED');
87 |       expect(result.canRetry).toBe(true);
88 |     });
89 | 
90 |     it('should handle missing token', async () => {
91 |       (tokenManager as any).loadToken.mockResolvedValue(null);
92 | 
93 |       const result = await tokenManager.autoRenewToken('[email protected]');
94 |       expect(result.success).toBe(false);
95 |       expect(result.status).toBe('NO_TOKEN');
96 |     });
97 |   });
98 | });
99 | 
```

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

```typescript
  1 | import { google } from 'googleapis';
  2 | import { 
  3 |   GmailAttachment,
  4 |   IncomingGmailAttachment,
  5 |   OutgoingGmailAttachment,
  6 |   GmailError 
  7 | } from '../types.js';
  8 | import { AttachmentIndexService } from '../../attachments/index-service.js';
  9 | 
 10 | export class GmailAttachmentService {
 11 |   private static instance: GmailAttachmentService;
 12 |   private indexService: AttachmentIndexService;
 13 |   private gmailClient?: ReturnType<typeof google.gmail>;
 14 | 
 15 |   private constructor() {
 16 |     this.indexService = AttachmentIndexService.getInstance();
 17 |   }
 18 | 
 19 |   /**
 20 |    * Add attachment metadata to the index
 21 |    */
 22 |   addAttachment(messageId: string, attachment: {
 23 |     id: string;
 24 |     name: string;
 25 |     mimeType: string;
 26 |     size: number;
 27 |   }): void {
 28 |     this.indexService.addAttachment(messageId, attachment);
 29 |   }
 30 | 
 31 |   public static getInstance(): GmailAttachmentService {
 32 |     if (!GmailAttachmentService.instance) {
 33 |       GmailAttachmentService.instance = new GmailAttachmentService();
 34 |     }
 35 |     return GmailAttachmentService.instance;
 36 |   }
 37 | 
 38 |   /**
 39 |    * Updates the Gmail client instance
 40 |    */
 41 |   updateClient(client: ReturnType<typeof google.gmail>) {
 42 |     this.gmailClient = client;
 43 |   }
 44 | 
 45 |   private ensureClient(): ReturnType<typeof google.gmail> {
 46 |     if (!this.gmailClient) {
 47 |       throw new GmailError(
 48 |         'Gmail client not initialized',
 49 |         'CLIENT_ERROR',
 50 |         'Please ensure the service is initialized'
 51 |       );
 52 |     }
 53 |     return this.gmailClient;
 54 |   }
 55 | 
 56 |   /**
 57 |    * Get attachment content from Gmail
 58 |    */
 59 |   async getAttachment(
 60 |     email: string,
 61 |     messageId: string,
 62 |     filename: string
 63 |   ): Promise<IncomingGmailAttachment> {
 64 |     try {
 65 |       // Get original metadata from index
 66 |       const metadata = this.indexService.getMetadata(messageId, filename);
 67 |       if (!metadata) {
 68 |         throw new GmailError(
 69 |           'Attachment not found',
 70 |           'ATTACHMENT_ERROR',
 71 |           'Attachment metadata not found - message may need to be refreshed'
 72 |         );
 73 |       }
 74 | 
 75 |       const client = this.ensureClient();
 76 |       const { data } = await client.users.messages.attachments.get({
 77 |         userId: 'me',
 78 |         messageId,
 79 |         id: metadata.originalId,
 80 |       });
 81 | 
 82 |       if (!data.data) {
 83 |         throw new Error('No attachment data received');
 84 |       }
 85 | 
 86 |       return {
 87 |         id: metadata.originalId,
 88 |         content: data.data,
 89 |         size: metadata.size,
 90 |         name: metadata.filename,
 91 |         mimeType: metadata.mimeType,
 92 |       };
 93 |     } catch (error) {
 94 |       throw new GmailError(
 95 |         'Failed to get attachment',
 96 |         'ATTACHMENT_ERROR',
 97 |         `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
 98 |       );
 99 |     }
100 |   }
101 | 
102 |   /**
103 |    * Validate attachment content and size
104 |    */
105 |   validateAttachment(attachment: OutgoingGmailAttachment): void {
106 |     if (!attachment.content) {
107 |       throw new GmailError(
108 |         'Invalid attachment',
109 |         'VALIDATION_ERROR',
110 |         'Attachment content is required'
111 |       );
112 |     }
113 | 
114 |     // Gmail's attachment size limit is 25MB
115 |     const MAX_SIZE = 25 * 1024 * 1024;
116 |     if (attachment.size > MAX_SIZE) {
117 |       throw new GmailError(
118 |         'Invalid attachment',
119 |         'VALIDATION_ERROR',
120 |         `Attachment size ${attachment.size} exceeds maximum allowed size ${MAX_SIZE}`
121 |       );
122 |     }
123 |   }
124 | 
125 |   /**
126 |    * Prepare attachment for sending
127 |    */
128 |   prepareAttachment(attachment: OutgoingGmailAttachment): {
129 |     filename: string;
130 |     mimeType: string;
131 |     content: string;
132 |   } {
133 |     this.validateAttachment(attachment);
134 |     
135 |     return {
136 |       filename: attachment.name,
137 |       mimeType: attachment.mimeType,
138 |       content: attachment.content,
139 |     };
140 |   }
141 | }
142 | 
```

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

```typescript
  1 | import { AttachmentIndexService } from './index-service.js';
  2 | 
  3 | /**
  4 |  * Service for managing attachment cleanup with intelligent scheduling
  5 |  */
  6 | export class AttachmentCleanupService {
  7 |   private cleanupInterval: NodeJS.Timeout | null = null;
  8 |   private readonly baseIntervalMs = 300000; // 5 minutes
  9 |   private readonly maxIntervalMs = 3600000; // 1 hour
 10 |   private currentIntervalMs: number;
 11 |   private lastCleanupTime: number = 0;
 12 |   private lastIndexSize: number = 0;
 13 | 
 14 |   constructor(private indexService: AttachmentIndexService) {
 15 |     this.currentIntervalMs = this.baseIntervalMs;
 16 |   }
 17 | 
 18 |   /**
 19 |    * Start the cleanup service with adaptive scheduling
 20 |    */
 21 |   start(): void {
 22 |     if (this.cleanupInterval) {
 23 |       return; // Already running
 24 |     }
 25 | 
 26 |     // Initial state
 27 |     this.lastIndexSize = this.indexService.size;
 28 |     this.lastCleanupTime = Date.now();
 29 | 
 30 |     // Run initial cleanup immediately
 31 |     this.cleanup();
 32 | 
 33 |     // Schedule next cleanup
 34 |     this.scheduleNextCleanup();
 35 |   }
 36 | 
 37 |   /**
 38 |    * Schedule next cleanup based on system activity
 39 |    */
 40 |   private scheduleNextCleanup(): void {
 41 |     if (this.cleanupInterval) {
 42 |       clearTimeout(this.cleanupInterval);
 43 |     }
 44 | 
 45 |     // Calculate next interval based on activity
 46 |     const nextInterval = this.currentIntervalMs;
 47 |     
 48 |     this.cleanupInterval = setTimeout(() => {
 49 |       this.cleanup();
 50 |       this.scheduleNextCleanup();
 51 |     }, nextInterval);
 52 |   }
 53 | 
 54 |   /**
 55 |    * Get current cleanup interval (for testing)
 56 |    */
 57 |   getCurrentInterval(): number {
 58 |     return this.currentIntervalMs;
 59 |   }
 60 | 
 61 |   /**
 62 |    * Notify the cleanup service of system activity
 63 |    * Call this when attachments are added or accessed
 64 |    */
 65 |   notifyActivity(): void {
 66 |     const currentSize = this.indexService.size;
 67 |     const sizeIncreased = currentSize > this.lastIndexSize;
 68 |     
 69 |     // Decrease interval if we're seeing increased activity
 70 |     if (sizeIncreased) {
 71 |       this.currentIntervalMs = Math.max(
 72 |         this.baseIntervalMs,
 73 |         this.currentIntervalMs * 0.75
 74 |       );
 75 |       
 76 |       // Force immediate cleanup if we're near capacity
 77 |       if (currentSize >= this.indexService.maxEntries * 0.9) {
 78 |         this.cleanup();
 79 |         this.scheduleNextCleanup();
 80 |       }
 81 |     } else {
 82 |       // Gradually increase interval during low activity
 83 |       this.currentIntervalMs = Math.min(
 84 |         this.maxIntervalMs,
 85 |         this.currentIntervalMs * 1.25
 86 |       );
 87 |     }
 88 | 
 89 |     this.lastIndexSize = currentSize;
 90 |   }
 91 | 
 92 |   /**
 93 |    * Stop the cleanup service
 94 |    */
 95 |   stop(): void {
 96 |     if (this.cleanupInterval) {
 97 |       clearTimeout(this.cleanupInterval);
 98 |       this.cleanupInterval = null;
 99 |     }
100 |   }
101 | 
102 |   /**
103 |    * For testing purposes only - clear all internal state
104 |    */
105 |   _reset(): void {
106 |     this.stop();
107 |     this.currentIntervalMs = this.baseIntervalMs;
108 |     this.lastCleanupTime = 0;
109 |     this.lastIndexSize = 0;
110 |   }
111 | 
112 |   /**
113 |    * Run cleanup with performance monitoring
114 |    */
115 |   private cleanup(): void {
116 |     try {
117 |       const startTime = process.hrtime();
118 |       
119 |       // Only run if enough time has passed since last cleanup
120 |       const timeSinceLastCleanup = Date.now() - this.lastCleanupTime;
121 |       if (timeSinceLastCleanup < this.baseIntervalMs / 2) {
122 |         return;
123 |       }
124 | 
125 |       // Run cleanup
126 |       this.indexService.cleanExpiredEntries();
127 |       this.lastCleanupTime = Date.now();
128 | 
129 |       // Monitor performance
130 |       const [seconds, nanoseconds] = process.hrtime(startTime);
131 |       const milliseconds = seconds * 1000 + nanoseconds / 1000000;
132 |       
133 |       // Adjust interval based on cleanup duration
134 |       if (milliseconds > 100) { // If cleanup takes >100ms
135 |         this.currentIntervalMs = Math.min(
136 |           this.maxIntervalMs,
137 |           this.currentIntervalMs * 1.5
138 |         );
139 |       }
140 |     } catch (error) {
141 |       console.error('Error during attachment cleanup:', error);
142 |     }
143 |   }
144 | }
145 | 
```

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

```bash
  1 | #!/bin/bash
  2 | set -e
  3 | 
  4 | # Force immediate output
  5 | exec 1>&1
  6 | 
  7 | # This script provides a lightweight local development build pipeline
  8 | # that mirrors our CI approach but optimized for speed and local iteration.
  9 | # It performs all build steps from linting through Docker image creation,
 10 | # providing minimal but clear status output.
 11 | 
 12 | # Parse command line arguments
 13 | VERBOSE=false
 14 | IMAGE_TAG="google-workspace-mcp:local"
 15 | while [[ "$#" -gt 0 ]]; do
 16 |     case $1 in
 17 |         --verbose) VERBOSE=true ;;
 18 |         --tag) IMAGE_TAG="$2"; shift ;;
 19 |         *) echo "Unknown parameter: $1"; exit 1 ;;
 20 |     esac
 21 |     shift
 22 | done
 23 | 
 24 | # Create temp directory for logs if it doesn't exist
 25 | TEMP_DIR="/tmp/google-workspace-mcp"
 26 | mkdir -p "$TEMP_DIR"
 27 | 
 28 | # Setup colored output
 29 | GREEN='\033[0;32m'
 30 | RED='\033[0;31m'
 31 | YELLOW='\033[1;33m'
 32 | NC='\033[0m' # No Color
 33 | CHECK_MARK="✓"
 34 | X_MARK="✗"
 35 | WARNING_MARK="⚠"
 36 | 
 37 | # Function to check log file size and show warning if needed
 38 | check_log_size() {
 39 |     local log_file=$1
 40 |     if [ -f "$log_file" ]; then
 41 |         local line_count=$(wc -l < "$log_file")
 42 |         if [ $line_count -gt 100 ]; then
 43 |             echo -e "${YELLOW}${WARNING_MARK} Large log file detected ($line_count lines)${NC}"
 44 |             echo "  Tips for viewing large logs:"
 45 |             echo "  • head -n 20 $log_file     (view first 20 lines)"
 46 |             echo "  • tail -n 20 $log_file     (view last 20 lines)"
 47 |             echo "  • less $log_file           (scroll through file)"
 48 |             echo "  • grep 'error' $log_file   (search for specific terms)"
 49 |             echo "  • use pageless mode with tools when viewing files"
 50 |         fi
 51 |     fi
 52 | }
 53 | 
 54 | # Function to run a step and show its status
 55 | run_step() {
 56 |     local step_name=$1
 57 |     local log_file="$TEMP_DIR/$2.log"
 58 |     local command=$3
 59 | 
 60 |     echo -n "→ $step_name... "
 61 | 
 62 |     if [ "$VERBOSE" = true ]; then
 63 |         if eval "$command"; then
 64 |             echo -e "${GREEN}${CHECK_MARK} Success${NC}"
 65 |             return 0
 66 |         else
 67 |             echo -e "${RED}${X_MARK} Failed${NC}"
 68 |             return 1
 69 |         fi
 70 |     else
 71 |         if eval "$command > '$log_file' 2>&1"; then
 72 |             echo -e "${GREEN}${CHECK_MARK} Success${NC} (log: $log_file)"
 73 |             check_log_size "$log_file"
 74 |             return 0
 75 |         else
 76 |             echo -e "${RED}${X_MARK} Failed${NC} (see details in $log_file)"
 77 |             check_log_size "$log_file"
 78 |             return 1
 79 |         fi
 80 |     fi
 81 | }
 82 | 
 83 | # Install dependencies
 84 | run_step "Installing dependencies" "npm-install" "npm install" || exit 1
 85 | 
 86 | # Type check
 87 | run_step "Type checking" "type-check" "npm run type-check" || exit 1
 88 | 
 89 | # Run linting
 90 | run_step "Linting" "lint" "npm run lint" || exit 1
 91 | 
 92 | # Run tests (commented out since they're a placeholder)
 93 | # run_step "Testing" "test" "npm run test || true"
 94 | 
 95 | # Build TypeScript
 96 | run_step "Building TypeScript" "build" "npm run build" || exit 1
 97 | 
 98 | # Build Docker image using the local Dockerfile
 99 | echo "→ Building Docker image..."
100 | if [ "$VERBOSE" = true ]; then
101 |     if docker build -t "$IMAGE_TAG" -f "Dockerfile.local" .; then
102 |         echo -e "${GREEN}${CHECK_MARK} Docker build successful${NC}"
103 |     else
104 |         echo -e "${RED}${X_MARK} Docker build failed${NC}"
105 |         exit 1
106 |     fi
107 | else
108 |     DOCKER_LOG="$TEMP_DIR/docker-build.log"
109 |     if docker build -t "$IMAGE_TAG" -f "Dockerfile.local" . > "$DOCKER_LOG" 2>&1; then
110 |         echo -e "${GREEN}${CHECK_MARK} Docker build successful${NC} (log: $DOCKER_LOG)"
111 |         check_log_size "$DOCKER_LOG"
112 |     else
113 |         echo -e "${RED}${X_MARK} Docker build failed${NC} (see details in $DOCKER_LOG)"
114 |         check_log_size "$DOCKER_LOG"
115 |         exit 1
116 |     fi
117 | fi
118 | 
119 | echo -e "\n${GREEN}Build complete!${NC} Image tagged as $IMAGE_TAG"
120 | echo "To change the image tag, use: ./scripts/build-local.sh --tag <your-tag>"
121 | echo "This build uses Dockerfile.local which is optimized for local development."
122 | 
```

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

```typescript
  1 | import { google } from 'googleapis';
  2 | import {
  3 |   GetGmailSettingsParams,
  4 |   GetGmailSettingsResponse,
  5 |   GmailError
  6 | } from '../types.js';
  7 | 
  8 | export class SettingsService {
  9 |   constructor(
 10 |     private gmailClient?: ReturnType<typeof google.gmail>
 11 |   ) {}
 12 | 
 13 |   /**
 14 |    * Updates the Gmail client instance
 15 |    * @param client - New Gmail client instance
 16 |    */
 17 |   updateClient(client: ReturnType<typeof google.gmail>) {
 18 |     this.gmailClient = client;
 19 |   }
 20 | 
 21 |   private ensureClient(): ReturnType<typeof google.gmail> {
 22 |     if (!this.gmailClient) {
 23 |       throw new GmailError(
 24 |         'Gmail client not initialized',
 25 |         'CLIENT_ERROR',
 26 |         'Please ensure the service is initialized'
 27 |       );
 28 |     }
 29 |     return this.gmailClient;
 30 |   }
 31 | 
 32 |   async getWorkspaceGmailSettings({ email }: GetGmailSettingsParams): Promise<GetGmailSettingsResponse> {
 33 |     try {
 34 |       // Get profile data
 35 |       const client = this.ensureClient();
 36 |       const { data: profile } = await client.users.getProfile({
 37 |         userId: 'me'
 38 |       });
 39 | 
 40 |       // Get settings data
 41 |       const [
 42 |         { data: autoForwarding },
 43 |         { data: imap },
 44 |         { data: language },
 45 |         { data: pop },
 46 |         { data: vacation }
 47 |       ] = await Promise.all([
 48 |         client.users.settings.getAutoForwarding({ userId: 'me' }),
 49 |         client.users.settings.getImap({ userId: 'me' }),
 50 |         client.users.settings.getLanguage({ userId: 'me' }),
 51 |         client.users.settings.getPop({ userId: 'me' }),
 52 |         client.users.settings.getVacation({ userId: 'me' })
 53 |       ]);
 54 | 
 55 |       const response: GetGmailSettingsResponse = {
 56 |         profile: {
 57 |           emailAddress: profile.emailAddress ?? '',
 58 |           messagesTotal: typeof profile.messagesTotal === 'number' ? profile.messagesTotal : 0,
 59 |           threadsTotal: typeof profile.threadsTotal === 'number' ? profile.threadsTotal : 0,
 60 |           historyId: profile.historyId ?? ''
 61 |         },
 62 |         settings: {
 63 |           ...(language?.displayLanguage && {
 64 |             language: {
 65 |               displayLanguage: language.displayLanguage
 66 |             }
 67 |           }),
 68 |           ...(autoForwarding && {
 69 |             autoForwarding: {
 70 |               enabled: Boolean(autoForwarding.enabled),
 71 |               ...(autoForwarding.emailAddress && {
 72 |                 emailAddress: autoForwarding.emailAddress
 73 |               })
 74 |             }
 75 |           }),
 76 |           ...(imap && {
 77 |             imap: {
 78 |               enabled: Boolean(imap.enabled),
 79 |               ...(typeof imap.autoExpunge === 'boolean' && {
 80 |                 autoExpunge: imap.autoExpunge
 81 |               }),
 82 |               ...(imap.expungeBehavior && {
 83 |                 expungeBehavior: imap.expungeBehavior
 84 |               })
 85 |             }
 86 |           }),
 87 |           ...(pop && {
 88 |             pop: {
 89 |               enabled: Boolean(pop.accessWindow),
 90 |               ...(pop.accessWindow && {
 91 |                 accessWindow: pop.accessWindow
 92 |               })
 93 |             }
 94 |           }),
 95 |           ...(vacation && {
 96 |             vacationResponder: {
 97 |               enabled: Boolean(vacation.enableAutoReply),
 98 |               ...(vacation.startTime && {
 99 |                 startTime: vacation.startTime
100 |               }),
101 |               ...(vacation.endTime && {
102 |                 endTime: vacation.endTime
103 |               }),
104 |               ...(vacation.responseSubject && {
105 |                 responseSubject: vacation.responseSubject
106 |               }),
107 |               ...((vacation.responseBodyHtml || vacation.responseBodyPlainText) && {
108 |                 message: vacation.responseBodyHtml ?? vacation.responseBodyPlainText ?? ''
109 |               })
110 |             }
111 |           })
112 |         }
113 |       };
114 | 
115 |       return response;
116 |     } catch (error) {
117 |       if (error instanceof GmailError) {
118 |         throw error;
119 |       }
120 |       throw new GmailError(
121 |         'Failed to get Gmail settings',
122 |         'SETTINGS_ERROR',
123 |         `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
124 |       );
125 |     }
126 |   }
127 | }
128 | 
```
Page 1/4FirstPrevNextLast