├── mcp-config.json ├── docker-compose.yml ├── tsconfig.json ├── .npmignore ├── .gitignore ├── Dockerfile ├── smithery.yaml ├── LICENSE ├── filter-examples.md ├── package.json ├── llms-install.md ├── src ├── evals │ └── evals.ts ├── utl.ts ├── filter-manager.ts ├── label-manager.ts └── index.ts ├── setup.js └── README.md /mcp-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "gmail": { 4 | "command": "node", 5 | "args": [ 6 | "D:\\BackDataService\\Gmail-MCP-Server\\dist\\index.js" 7 | ] 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | gmail-mcp: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | volumes: 9 | - mcp-gmail:/gmail-server 10 | environment: 11 | - GMAIL_CREDENTIALS_PATH=/gmail-server/credentials.json 12 | ports: 13 | - "3000:3000" 14 | restart: unless-stopped 15 | 16 | volumes: 17 | mcp-gmail: -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Source 2 | src/ 3 | 4 | # Development 5 | .git/ 6 | .github/ 7 | .gitignore 8 | .npmrc 9 | .env 10 | .env.* 11 | 12 | # IDE 13 | .vscode/ 14 | .idea/ 15 | *.swp 16 | *.swo 17 | 18 | # Logs 19 | logs/ 20 | *.log 21 | npm-debug.log* 22 | 23 | # Dependencies 24 | node_modules/ 25 | 26 | # Build process 27 | tsconfig.json 28 | .eslintrc 29 | .prettierrc 30 | 31 | # Tests 32 | test/ 33 | coverage/ 34 | .nyc_output/ 35 | 36 | # Misc 37 | .DS_Store 38 | Thumbs.db -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Build 8 | build/ 9 | dist/ 10 | *.tsbuildinfo 11 | 12 | # Environment 13 | .env 14 | .env.local 15 | .env.*.local 16 | 17 | # IDE 18 | .idea/ 19 | .vscode/ 20 | *.swp 21 | *.swo 22 | 23 | # OS 24 | .DS_Store 25 | Thumbs.db 26 | 27 | # Project specific 28 | gcp-oauth.keys.json 29 | .calendar-mcp/ 30 | credentials.json 31 | .calendar-server-credentials.json 32 | .gcp-oauth.keys.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-slim 2 | 3 | WORKDIR /app 4 | 5 | # Copy package files 6 | COPY package.json package-lock.json* ./ 7 | 8 | # Copy source files and config first 9 | COPY tsconfig.json ./ 10 | COPY src ./src 11 | 12 | # Install dependencies (which will trigger build via prepare script) 13 | RUN npm ci 14 | 15 | # Create directory for credentials and config 16 | RUN mkdir -p /gmail-server /root/.gmail-mcp 17 | 18 | # Set environment variables 19 | ENV NODE_ENV=production 20 | ENV GMAIL_CREDENTIALS_PATH=/gmail-server/credentials.json 21 | ENV GMAIL_OAUTH_PATH=/root/.gmail-mcp/gcp-oauth.keys.json 22 | 23 | # Expose port for OAuth flow 24 | EXPOSE 3000 25 | 26 | # Set entrypoint command 27 | ENTRYPOINT ["node", "dist/index.js"] -------------------------------------------------------------------------------- /smithery.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 | - gcpOauthKeysPath 10 | - credentialsPath 11 | properties: 12 | gcpOauthKeysPath: 13 | type: string 14 | description: Path to the GCP OAuth keys JSON file 15 | credentialsPath: 16 | type: string 17 | description: Path to the stored credentials JSON file 18 | commandFunction: 19 | # A function that produces the CLI command to start the MCP on stdio. 20 | |- 21 | (config) => ({command:'node',args:['dist/index.js'],env:{GMAIL_OAUTH_PATH:config.gcpOauthKeysPath, GMAIL_CREDENTIALS_PATH:config.credentialsPath}}) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 GongRzhe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /filter-examples.md: -------------------------------------------------------------------------------- 1 | # Gmail Filter Management Examples 2 | 3 | This document provides practical examples of how to use the new Gmail filter functionality in the MCP server. 4 | 5 | ## Quick Start 6 | 7 | After setting up authentication and adding the required scope, you can start creating filters to automate your email management. 8 | 9 | ## Common Use Cases 10 | 11 | ### 1. Newsletter Management 12 | Automatically organize newsletters: 13 | 14 | ``` 15 | Template: fromSender 16 | Parameters: senderEmail, labelIds, archive 17 | ``` 18 | 19 | ### 2. Work Email Organization 20 | Create filters for work emails from managers and team notifications. 21 | 22 | ### 3. Automated Filing 23 | Set up filters to automatically file financial emails and support tickets. 24 | 25 | ### 4. Large Attachment Management 26 | Handle emails with large attachments by applying special labels. 27 | 28 | ## Best Practices 29 | 30 | 1. Start Simple: Begin with basic filters using templates 31 | 2. Test Criteria: Use search_emails to test filter criteria first 32 | 3. Use Labels Strategically: Create logical label hierarchy 33 | 4. Review Regularly: Clean up unused filters periodically 34 | 5. Combine with Batch Operations: Apply filters retroactively 35 | 36 | ## Limitations 37 | 38 | - Maximum of 1,000 filters per Gmail account 39 | - Only one user-defined label can be added per filter 40 | - Forwarding requires verified destination addresses 41 | - Some system labels cannot be removed -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gongrzhe/server-gmail-autoauth-mcp", 3 | "version": "1.1.11", 4 | "description": "Gmail MCP server with auto authentication support", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "bin": { 8 | "gmail-mcp": "./dist/index.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "start": "node dist/index.js", 13 | "auth": "node dist/index.js auth", 14 | "prepare": "npm run build", 15 | "prepublishOnly": "npm run build" 16 | }, 17 | "files": [ 18 | "dist", 19 | "README.md" 20 | ], 21 | "keywords": [ 22 | "gmail", 23 | "mcp", 24 | "cursor", 25 | "ai", 26 | "oauth", 27 | "model-context-protocol", 28 | "google-gmail", 29 | "claude", 30 | "auto-auth" 31 | ], 32 | "author": "gongrzhe", 33 | "license": "ISC", 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/gongrzhe/server-gmail-autoauth-mcp.git" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/gongrzhe/server-gmail-autoauth-mcp/issues" 40 | }, 41 | "homepage": "https://github.com/gongrzhe/server-gmail-autoauth-mcp#readme", 42 | "publishConfig": { 43 | "access": "public" 44 | }, 45 | "engines": { 46 | "node": ">=14.0.0" 47 | }, 48 | "dependencies": { 49 | "@modelcontextprotocol/sdk": "^0.4.0", 50 | "@types/mime-types": "^2.1.4", 51 | "google-auth-library": "^9.4.1", 52 | "googleapis": "^129.0.0", 53 | "mcp-evals": "^1.0.18", 54 | "mime-types": "^3.0.1", 55 | "nodemailer": "^7.0.3", 56 | "open": "^10.0.0", 57 | "zod": "^3.22.4", 58 | "zod-to-json-schema": "^3.22.1" 59 | }, 60 | "devDependencies": { 61 | "@types/node": "^20.10.5", 62 | "@types/nodemailer": "^6.4.17", 63 | "typescript": "^5.3.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /llms-install.md: -------------------------------------------------------------------------------- 1 | # Gmail AutoAuth MCP Installation Guide 2 | 3 | This guide will help you install and configure the Gmail AutoAuth MCP server for managing Gmail operations through Claude Desktop with auto authentication support. 4 | 5 | ## Requirements 6 | 7 | - Node.js and npm installed 8 | - Access to create a Google Cloud Project 9 | - Local directory for configuration storage 10 | - Web browser for OAuth authentication 11 | 12 | ## Installation Steps 13 | 14 | 1. First, create a Google Cloud Project and obtain the necessary credentials: 15 | ``` 16 | 1. Go to Google Cloud Console (https://console.cloud.google.com) 17 | 2. Create a new project or select an existing one 18 | 3. Enable the Gmail API for your project 19 | 4. Create OAuth 2.0 credentials: 20 | - Go to "APIs & Services" > "Credentials" 21 | - Click "Create Credentials" > "OAuth client ID" 22 | - Choose "Desktop app" or "Web application" type 23 | - For Web application, add http://localhost:3000/oauth2callback to redirect URIs 24 | - Download the OAuth keys JSON file 25 | - Rename it to gcp-oauth.keys.json 26 | ``` 27 | 28 | 2. Set up the configuration directory: 29 | ```bash 30 | mkdir -p ~/.gmail-mcp 31 | mv gcp-oauth.keys.json ~/.gmail-mcp/ 32 | ``` 33 | 34 | 3. Run authentication: 35 | ```bash 36 | npx @gongrzhe/server-gmail-autoauth-mcp auth 37 | ``` 38 | This will: 39 | - Look for gcp-oauth.keys.json in current directory or ~/.gmail-mcp/ 40 | - Copy it to ~/.gmail-mcp/ if found in current directory 41 | - Launch browser for Google authentication 42 | - Save credentials as ~/.gmail-mcp/credentials.json 43 | 44 | 4. Configure Claude Desktop by adding the MCP server configuration: 45 | ```json 46 | { 47 | "mcpServers": { 48 | "gmail": { 49 | "command": "npx", 50 | "args": [ 51 | "@gongrzhe/server-gmail-autoauth-mcp" 52 | ] 53 | } 54 | } 55 | } 56 | ``` 57 | 58 | ## Troubleshooting 59 | 60 | If you encounter any issues during installation: 61 | 62 | 1. OAuth Keys Issues: 63 | - Verify gcp-oauth.keys.json exists in correct location 64 | - Check file permissions 65 | - Ensure keys contain valid web or installed credentials 66 | 67 | 2. Authentication Errors: 68 | - Confirm Gmail API is enabled 69 | - For web applications, verify redirect URI configuration 70 | - Check port 3000 is available during authentication 71 | 72 | 3. Configuration Issues: 73 | - Verify ~/.gmail-mcp directory exists and has correct permissions 74 | - Check credentials.json was created after authentication 75 | - Ensure Claude Desktop configuration is properly formatted 76 | 77 | ## Security Notes 78 | 79 | - Store OAuth credentials securely in ~/.gmail-mcp/ 80 | - Never commit credentials to version control 81 | - Use proper file permissions for config directory 82 | - Regularly review access in Google Account settings 83 | - Credentials are only accessible by current user 84 | 85 | ## Usage Examples 86 | 87 | After installation, you can perform various Gmail operations: 88 | 89 | ### Send Email 90 | ```json 91 | { 92 | "to": ["recipient@example.com"], 93 | "subject": "Meeting Tomorrow", 94 | "body": "Hi,\n\nJust a reminder about our meeting tomorrow at 10 AM.\n\nBest regards", 95 | "cc": ["cc@example.com"], 96 | "bcc": ["bcc@example.com"] 97 | } 98 | ``` 99 | 100 | ### Search Emails 101 | ```json 102 | { 103 | "query": "from:sender@example.com after:2024/01/01", 104 | "maxResults": 10 105 | } 106 | ``` 107 | 108 | ### Manage Email 109 | - Read emails by ID 110 | - Move emails between labels 111 | - Mark emails as read/unread 112 | - Delete emails 113 | - List emails in different folders 114 | 115 | For more details or support, please check the GitHub repository or file an issue. -------------------------------------------------------------------------------- /src/evals/evals.ts: -------------------------------------------------------------------------------- 1 | //evals.ts 2 | 3 | import { EvalConfig } from 'mcp-evals'; 4 | import { openai } from "@ai-sdk/openai"; 5 | import { grade, EvalFunction } from "mcp-evals"; 6 | 7 | const send_emailEval: EvalFunction = { 8 | name: "send_emailEval", 9 | description: "Evaluates sending a new email", 10 | run: async () => { 11 | const result = await grade(openai("gpt-4"), "Please send an email to example@domain.com with the subject 'Meeting Reminder' and a short message confirming our meeting tomorrow, politely requesting confirmation of attendance."); 12 | return JSON.parse(result); 13 | } 14 | }; 15 | 16 | const draft_email: EvalFunction = { 17 | name: 'draft_email', 18 | description: 'Evaluates the tool ability to draft an email', 19 | run: async () => { 20 | const result = await grade(openai("gpt-4"), "Draft a new email to my manager requesting a meeting to discuss project updates and timelines."); 21 | return JSON.parse(result); 22 | } 23 | }; 24 | 25 | const read_emailEval: EvalFunction = { 26 | name: 'read_email Tool Evaluation', 27 | description: 'Evaluates retrieving the content of a specific email', 28 | run: async () => { 29 | const result = await grade(openai("gpt-4"), "Please retrieve the content of the email with the subject 'Upcoming Meeting' from my inbox."); 30 | return JSON.parse(result); 31 | } 32 | }; 33 | 34 | const search_emailsEval: EvalFunction = { 35 | name: "search_emails Tool Evaluation", 36 | description: "Evaluates the tool ability to search emails using Gmail syntax", 37 | run: async () => { 38 | const result = await grade(openai("gpt-4"), "Search my mailbox for unread emails from boss@company.com that have attachments. Provide the Gmail search syntax."); 39 | return JSON.parse(result); 40 | } 41 | }; 42 | 43 | const modify_emailEval: EvalFunction = { 44 | name: 'modify_email Tool Evaluation', 45 | description: 'Evaluates the modify_email tool functionality', 46 | run: async () => { 47 | const result = await grade(openai("gpt-4"), "Please move the email labeled 'Work' to the 'Important' folder and remove the 'unread' label."); 48 | return JSON.parse(result); 49 | } 50 | }; 51 | 52 | // New filter management evaluations 53 | const create_filterEval: EvalFunction = { 54 | name: 'create_filter Tool Evaluation', 55 | description: 'Evaluates creating a custom Gmail filter', 56 | run: async () => { 57 | const result = await grade(openai("gpt-4"), "Create a filter that automatically labels emails from newsletter@company.com with 'Newsletter' label and archives them (skips inbox)."); 58 | return JSON.parse(result); 59 | } 60 | }; 61 | 62 | const create_filter_templateEval: EvalFunction = { 63 | name: 'create_filter_template Tool Evaluation', 64 | description: 'Evaluates creating filters using predefined templates', 65 | run: async () => { 66 | const result = await grade(openai("gpt-4"), "Create a filter using a template to automatically handle all emails from notifications@github.com by labeling them as 'GitHub' and archiving them."); 67 | return JSON.parse(result); 68 | } 69 | }; 70 | 71 | const list_filtersEval: EvalFunction = { 72 | name: 'list_filters Tool Evaluation', 73 | description: 'Evaluates listing all Gmail filters', 74 | run: async () => { 75 | const result = await grade(openai("gpt-4"), "Show me all my current Gmail filters and their configurations."); 76 | return JSON.parse(result); 77 | } 78 | }; 79 | 80 | const filter_managementEval: EvalFunction = { 81 | name: 'filter_management Tool Evaluation', 82 | description: 'Evaluates comprehensive filter management operations', 83 | run: async () => { 84 | const result = await grade(openai("gpt-4"), "I want to create a filter for managing marketing emails. Filter emails containing 'unsubscribe' in the body, label them as 'Marketing', mark them as read, and skip the inbox."); 85 | return JSON.parse(result); 86 | } 87 | }; 88 | 89 | const config: EvalConfig = { 90 | model: openai("gpt-4"), 91 | evals: [ 92 | send_emailEval, 93 | draft_email, 94 | read_emailEval, 95 | search_emailsEval, 96 | modify_emailEval, 97 | create_filterEval, 98 | create_filter_templateEval, 99 | list_filtersEval, 100 | filter_managementEval 101 | ] 102 | }; 103 | 104 | export default config; 105 | 106 | export const evals = [ 107 | send_emailEval, 108 | draft_email, 109 | read_emailEval, 110 | search_emailsEval, 111 | modify_emailEval, 112 | create_filterEval, 113 | create_filter_templateEval, 114 | list_filtersEval, 115 | filter_managementEval 116 | ]; -------------------------------------------------------------------------------- /setup.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Basic debugging script 4 | console.log('====== Starting Setup Script ======'); 5 | 6 | try { 7 | // ES module imports 8 | console.log('Importing modules...'); 9 | import('fs').then(fs => { 10 | console.log('Successfully imported fs module'); 11 | 12 | import('path').then(path => { 13 | console.log('Successfully imported path module'); 14 | 15 | import('os').then(os => { 16 | console.log('Successfully imported os module'); 17 | 18 | import('child_process').then(({ execSync }) => { 19 | console.log('Successfully imported child_process module'); 20 | 21 | import('url').then(({ fileURLToPath }) => { 22 | console.log('Successfully imported url module'); 23 | 24 | // Get directory path 25 | const __filename = fileURLToPath(import.meta.url); 26 | const __dirname = path.dirname(__filename); 27 | console.log(`Current directory: ${__dirname}`); 28 | 29 | // Check Node.js version 30 | const nodeVersion = process.versions.node.split('.'); 31 | console.log(`Node.js version: ${process.versions.node}`); 32 | 33 | // Project path 34 | const basePath = path.resolve(__dirname); 35 | console.log(`Project path: ${basePath}`); 36 | 37 | // Create configuration 38 | console.log('Starting to create MCP configuration...'); 39 | const serverScriptPath = path.join(basePath, 'dist', 'index.js'); 40 | console.log(`Server script path: ${serverScriptPath}`); 41 | 42 | // Create configuration directory 43 | const configDir = path.join(os.homedir(), '.gmail-mcp'); 44 | console.log(`Configuration directory: ${configDir}`); 45 | 46 | if (!fs.existsSync(configDir)) { 47 | console.log('Creating configuration directory...'); 48 | fs.mkdirSync(configDir, { recursive: true }); 49 | console.log('Configuration directory created successfully'); 50 | } else { 51 | console.log('Configuration directory already exists'); 52 | } 53 | 54 | // Create MCP configuration 55 | const config = { 56 | "mcpServers": { 57 | "gmail": { 58 | "command": "node", 59 | "args": [serverScriptPath] 60 | } 61 | } 62 | }; 63 | 64 | // Save configuration 65 | const configPath = path.join(basePath, 'mcp-config.json'); 66 | console.log(`Saving configuration to: ${configPath}`); 67 | fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); 68 | console.log('Configuration saved'); 69 | 70 | // Get Claude Desktop configuration path 71 | let claudeConfigPath; 72 | if (process.platform === 'win32') { 73 | claudeConfigPath = path.join(process.env.APPDATA, 'Claude', 'claude_desktop_config.json'); 74 | } else if (process.platform === 'darwin') { 75 | claudeConfigPath = path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'); 76 | } else { 77 | claudeConfigPath = path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json'); 78 | } 79 | 80 | console.log(`Claude Desktop configuration path: ${claudeConfigPath}`); 81 | 82 | // Output instructions 83 | console.log('\n===== Setup Complete ====='); 84 | console.log(`MCP configuration written to: ${configPath}`); 85 | console.log('\nMCP configuration content:'); 86 | console.log(JSON.stringify(config, null, 2)); 87 | console.log(`\nPlease merge this configuration into the Claude Desktop configuration file: ${claudeConfigPath}`); 88 | console.log('\nBefore using the Gmail MCP server, you need to authenticate:'); 89 | console.log(`node ${serverScriptPath} auth`); 90 | 91 | console.log('\nSetup complete! You can now use the Gmail MCP server with compatible clients.'); 92 | }).catch(err => { 93 | console.error('Error importing url module:', err); 94 | }); 95 | }).catch(err => { 96 | console.error('Error importing child_process module:', err); 97 | }); 98 | }).catch(err => { 99 | console.error('Error importing os module:', err); 100 | }); 101 | }).catch(err => { 102 | console.error('Error importing path module:', err); 103 | }); 104 | }).catch(err => { 105 | console.error('Error importing fs module:', err); 106 | }); 107 | } catch (error) { 108 | console.error('Error executing script:', error); 109 | } 110 | -------------------------------------------------------------------------------- /src/utl.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { lookup as mimeLookup } from 'mime-types'; 4 | import nodemailer from 'nodemailer'; 5 | 6 | /** 7 | * Helper function to encode email headers containing non-ASCII characters 8 | * according to RFC 2047 MIME specification 9 | */ 10 | function encodeEmailHeader(text: string): string { 11 | // Only encode if the text contains non-ASCII characters 12 | if (/[^\x00-\x7F]/.test(text)) { 13 | // Use MIME Words encoding (RFC 2047) 14 | return '=?UTF-8?B?' + Buffer.from(text).toString('base64') + '?='; 15 | } 16 | return text; 17 | } 18 | 19 | export const validateEmail = (email: string): boolean => { 20 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 21 | return emailRegex.test(email); 22 | }; 23 | 24 | export function createEmailMessage(validatedArgs: any): string { 25 | const encodedSubject = encodeEmailHeader(validatedArgs.subject); 26 | // Determine content type based on available content and explicit mimeType 27 | let mimeType = validatedArgs.mimeType || 'text/plain'; 28 | 29 | // If htmlBody is provided and mimeType isn't explicitly set to text/plain, 30 | // use multipart/alternative to include both versions 31 | if (validatedArgs.htmlBody && mimeType !== 'text/plain') { 32 | mimeType = 'multipart/alternative'; 33 | } 34 | 35 | // Generate a random boundary string for multipart messages 36 | const boundary = `----=_NextPart_${Math.random().toString(36).substring(2)}`; 37 | 38 | // Validate email addresses 39 | (validatedArgs.to as string[]).forEach(email => { 40 | if (!validateEmail(email)) { 41 | throw new Error(`Recipient email address is invalid: ${email}`); 42 | } 43 | }); 44 | 45 | // Common email headers 46 | const emailParts = [ 47 | 'From: me', 48 | `To: ${validatedArgs.to.join(', ')}`, 49 | validatedArgs.cc ? `Cc: ${validatedArgs.cc.join(', ')}` : '', 50 | validatedArgs.bcc ? `Bcc: ${validatedArgs.bcc.join(', ')}` : '', 51 | `Subject: ${encodedSubject}`, 52 | // Add thread-related headers if specified 53 | validatedArgs.inReplyTo ? `In-Reply-To: ${validatedArgs.inReplyTo}` : '', 54 | validatedArgs.inReplyTo ? `References: ${validatedArgs.inReplyTo}` : '', 55 | 'MIME-Version: 1.0', 56 | ].filter(Boolean); 57 | 58 | // Construct the email based on the content type 59 | if (mimeType === 'multipart/alternative') { 60 | // Multipart email with both plain text and HTML 61 | emailParts.push(`Content-Type: multipart/alternative; boundary="${boundary}"`); 62 | emailParts.push(''); 63 | 64 | // Plain text part 65 | emailParts.push(`--${boundary}`); 66 | emailParts.push('Content-Type: text/plain; charset=UTF-8'); 67 | emailParts.push('Content-Transfer-Encoding: 7bit'); 68 | emailParts.push(''); 69 | emailParts.push(validatedArgs.body); 70 | emailParts.push(''); 71 | 72 | // HTML part 73 | emailParts.push(`--${boundary}`); 74 | emailParts.push('Content-Type: text/html; charset=UTF-8'); 75 | emailParts.push('Content-Transfer-Encoding: 7bit'); 76 | emailParts.push(''); 77 | emailParts.push(validatedArgs.htmlBody || validatedArgs.body); // Use body as fallback 78 | emailParts.push(''); 79 | 80 | // Close the boundary 81 | emailParts.push(`--${boundary}--`); 82 | } else if (mimeType === 'text/html') { 83 | // HTML-only email 84 | emailParts.push('Content-Type: text/html; charset=UTF-8'); 85 | emailParts.push('Content-Transfer-Encoding: 7bit'); 86 | emailParts.push(''); 87 | emailParts.push(validatedArgs.htmlBody || validatedArgs.body); 88 | } else { 89 | // Plain text email (default) 90 | emailParts.push('Content-Type: text/plain; charset=UTF-8'); 91 | emailParts.push('Content-Transfer-Encoding: 7bit'); 92 | emailParts.push(''); 93 | emailParts.push(validatedArgs.body); 94 | } 95 | 96 | return emailParts.join('\r\n'); 97 | } 98 | 99 | 100 | export async function createEmailWithNodemailer(validatedArgs: any): Promise { 101 | // Validate email addresses 102 | (validatedArgs.to as string[]).forEach(email => { 103 | if (!validateEmail(email)) { 104 | throw new Error(`Recipient email address is invalid: ${email}`); 105 | } 106 | }); 107 | 108 | // Create a nodemailer transporter (we won't actually send, just generate the message) 109 | const transporter = nodemailer.createTransport({ 110 | streamTransport: true, 111 | newline: 'unix', 112 | buffer: true 113 | }); 114 | 115 | // Prepare attachments for nodemailer 116 | const attachments = []; 117 | for (const filePath of validatedArgs.attachments) { 118 | if (!fs.existsSync(filePath)) { 119 | throw new Error(`File does not exist: ${filePath}`); 120 | } 121 | 122 | const fileName = path.basename(filePath); 123 | 124 | attachments.push({ 125 | filename: fileName, 126 | path: filePath 127 | }); 128 | } 129 | 130 | const mailOptions = { 131 | from: 'me', // Gmail API will replace this with the authenticated user 132 | to: validatedArgs.to.join(', '), 133 | cc: validatedArgs.cc?.join(', '), 134 | bcc: validatedArgs.bcc?.join(', '), 135 | subject: validatedArgs.subject, 136 | text: validatedArgs.body, 137 | html: validatedArgs.htmlBody, 138 | attachments: attachments, 139 | inReplyTo: validatedArgs.inReplyTo, 140 | references: validatedArgs.inReplyTo 141 | }; 142 | 143 | // Generate the raw message 144 | const info = await transporter.sendMail(mailOptions); 145 | const rawMessage = info.message.toString(); 146 | 147 | return rawMessage; 148 | } 149 | 150 | -------------------------------------------------------------------------------- /src/filter-manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filter Manager for Gmail MCP Server 3 | * Provides comprehensive filter management functionality 4 | */ 5 | 6 | // Type definitions for Gmail API filters 7 | export interface GmailFilterCriteria { 8 | from?: string; 9 | to?: string; 10 | subject?: string; 11 | query?: string; 12 | negatedQuery?: string; 13 | hasAttachment?: boolean; 14 | excludeChats?: boolean; 15 | size?: number; 16 | sizeComparison?: 'unspecified' | 'smaller' | 'larger'; 17 | } 18 | 19 | export interface GmailFilterAction { 20 | addLabelIds?: string[]; 21 | removeLabelIds?: string[]; 22 | forward?: string; 23 | } 24 | 25 | export interface GmailFilter { 26 | id?: string; 27 | criteria: GmailFilterCriteria; 28 | action: GmailFilterAction; 29 | } 30 | 31 | /** 32 | * Creates a new Gmail filter 33 | * @param gmail - Gmail API instance 34 | * @param criteria - Filter criteria to match messages 35 | * @param action - Actions to perform on matching messages 36 | * @returns The newly created filter 37 | */ 38 | export async function createFilter(gmail: any, criteria: GmailFilterCriteria, action: GmailFilterAction) { 39 | try { 40 | const filterBody: GmailFilter = { 41 | criteria, 42 | action 43 | }; 44 | 45 | const response = await gmail.users.settings.filters.create({ 46 | userId: 'me', 47 | requestBody: filterBody, 48 | }); 49 | 50 | return response.data; 51 | } catch (error: any) { 52 | if (error.code === 400) { 53 | throw new Error(`Invalid filter criteria or action: ${error.message}`); 54 | } 55 | throw new Error(`Failed to create filter: ${error.message}`); 56 | } 57 | } 58 | 59 | /** 60 | * Lists all Gmail filters 61 | * @param gmail - Gmail API instance 62 | * @returns Array of all filters 63 | */ 64 | export async function listFilters(gmail: any) { 65 | try { 66 | const response = await gmail.users.settings.filters.list({ 67 | userId: 'me', 68 | }); 69 | 70 | const filters = response.data.filters || []; 71 | 72 | return { 73 | filters, 74 | count: filters.length 75 | }; 76 | } catch (error: any) { 77 | throw new Error(`Failed to list filters: ${error.message}`); 78 | } 79 | } 80 | 81 | /** 82 | * Gets a specific Gmail filter by ID 83 | * @param gmail - Gmail API instance 84 | * @param filterId - ID of the filter to retrieve 85 | * @returns The filter details 86 | */ 87 | export async function getFilter(gmail: any, filterId: string) { 88 | try { 89 | const response = await gmail.users.settings.filters.get({ 90 | userId: 'me', 91 | id: filterId, 92 | }); 93 | 94 | return response.data; 95 | } catch (error: any) { 96 | if (error.code === 404) { 97 | throw new Error(`Filter with ID "${filterId}" not found.`); 98 | } 99 | throw new Error(`Failed to get filter: ${error.message}`); 100 | } 101 | } 102 | 103 | /** 104 | * Deletes a Gmail filter 105 | * @param gmail - Gmail API instance 106 | * @param filterId - ID of the filter to delete 107 | * @returns Success message 108 | */ 109 | export async function deleteFilter(gmail: any, filterId: string) { 110 | try { 111 | await gmail.users.settings.filters.delete({ 112 | userId: 'me', 113 | id: filterId, 114 | }); 115 | 116 | return { success: true, message: `Filter "${filterId}" deleted successfully.` }; 117 | } catch (error: any) { 118 | if (error.code === 404) { 119 | throw new Error(`Filter with ID "${filterId}" not found.`); 120 | } 121 | throw new Error(`Failed to delete filter: ${error.message}`); 122 | } 123 | } 124 | 125 | /** 126 | * Helper function to create common filter patterns 127 | */ 128 | export const filterTemplates = { 129 | /** 130 | * Filter emails from a specific sender 131 | */ 132 | fromSender: (senderEmail: string, labelIds: string[] = [], archive: boolean = false): { criteria: GmailFilterCriteria, action: GmailFilterAction } => ({ 133 | criteria: { from: senderEmail }, 134 | action: { 135 | addLabelIds: labelIds, 136 | removeLabelIds: archive ? ['INBOX'] : undefined 137 | } 138 | }), 139 | 140 | /** 141 | * Filter emails with specific subject 142 | */ 143 | withSubject: (subjectText: string, labelIds: string[] = [], markAsRead: boolean = false): { criteria: GmailFilterCriteria, action: GmailFilterAction } => ({ 144 | criteria: { subject: subjectText }, 145 | action: { 146 | addLabelIds: labelIds, 147 | removeLabelIds: markAsRead ? ['UNREAD'] : undefined 148 | } 149 | }), 150 | 151 | /** 152 | * Filter emails with attachments 153 | */ 154 | withAttachments: (labelIds: string[] = []): { criteria: GmailFilterCriteria, action: GmailFilterAction } => ({ 155 | criteria: { hasAttachment: true }, 156 | action: { addLabelIds: labelIds } 157 | }), 158 | 159 | /** 160 | * Filter large emails 161 | */ 162 | largeEmails: (sizeInBytes: number, labelIds: string[] = []): { criteria: GmailFilterCriteria, action: GmailFilterAction } => ({ 163 | criteria: { size: sizeInBytes, sizeComparison: 'larger' }, 164 | action: { addLabelIds: labelIds } 165 | }), 166 | 167 | /** 168 | * Filter emails containing specific text 169 | */ 170 | containingText: (searchText: string, labelIds: string[] = [], markImportant: boolean = false): { criteria: GmailFilterCriteria, action: GmailFilterAction } => ({ 171 | criteria: { query: `"${searchText}"` }, 172 | action: { 173 | addLabelIds: markImportant ? [...labelIds, 'IMPORTANT'] : labelIds 174 | } 175 | }), 176 | 177 | /** 178 | * Filter mailing list emails (common patterns) 179 | */ 180 | mailingList: (listIdentifier: string, labelIds: string[] = [], archive: boolean = true): { criteria: GmailFilterCriteria, action: GmailFilterAction } => ({ 181 | criteria: { query: `list:${listIdentifier} OR subject:[${listIdentifier}]` }, 182 | action: { 183 | addLabelIds: labelIds, 184 | removeLabelIds: archive ? ['INBOX'] : undefined 185 | } 186 | }) 187 | }; -------------------------------------------------------------------------------- /src/label-manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Label Manager for Gmail MCP Server 3 | * Provides comprehensive label management functionality 4 | */ 5 | 6 | // Type definitions for Gmail API labels 7 | export interface GmailLabel { 8 | id: string; 9 | name: string; 10 | type?: string; 11 | messageListVisibility?: string; 12 | labelListVisibility?: string; 13 | messagesTotal?: number; 14 | messagesUnread?: number; 15 | color?: { 16 | textColor?: string; 17 | backgroundColor?: string; 18 | }; 19 | } 20 | 21 | /** 22 | * Creates a new Gmail label 23 | * @param gmail - Gmail API instance 24 | * @param labelName - Name of the label to create 25 | * @param options - Optional settings for the label 26 | * @returns The newly created label 27 | */ 28 | export async function createLabel(gmail: any, labelName: string, options: { 29 | messageListVisibility?: string; 30 | labelListVisibility?: string; 31 | } = {}) { 32 | try { 33 | // Default visibility settings if not provided 34 | const messageListVisibility = options.messageListVisibility || 'show'; 35 | const labelListVisibility = options.labelListVisibility || 'labelShow'; 36 | 37 | const response = await gmail.users.labels.create({ 38 | userId: 'me', 39 | requestBody: { 40 | name: labelName, 41 | messageListVisibility, 42 | labelListVisibility, 43 | }, 44 | }); 45 | 46 | return response.data; 47 | } catch (error: any) { 48 | // Handle duplicate labels more gracefully 49 | if (error.message && error.message.includes('already exists')) { 50 | throw new Error(`Label "${labelName}" already exists. Please use a different name.`); 51 | } 52 | 53 | throw new Error(`Failed to create label: ${error.message}`); 54 | } 55 | } 56 | 57 | /** 58 | * Updates an existing Gmail label 59 | * @param gmail - Gmail API instance 60 | * @param labelId - ID of the label to update 61 | * @param updates - Properties to update 62 | * @returns The updated label 63 | */ 64 | export async function updateLabel(gmail: any, labelId: string, updates: { 65 | name?: string; 66 | messageListVisibility?: string; 67 | labelListVisibility?: string; 68 | }) { 69 | try { 70 | // Verify the label exists before updating 71 | await gmail.users.labels.get({ 72 | userId: 'me', 73 | id: labelId, 74 | }); 75 | 76 | const response = await gmail.users.labels.update({ 77 | userId: 'me', 78 | id: labelId, 79 | requestBody: updates, 80 | }); 81 | 82 | return response.data; 83 | } catch (error: any) { 84 | if (error.code === 404) { 85 | throw new Error(`Label with ID "${labelId}" not found.`); 86 | } 87 | 88 | throw new Error(`Failed to update label: ${error.message}`); 89 | } 90 | } 91 | 92 | /** 93 | * Deletes a Gmail label 94 | * @param gmail - Gmail API instance 95 | * @param labelId - ID of the label to delete 96 | * @returns Success message 97 | */ 98 | export async function deleteLabel(gmail: any, labelId: string) { 99 | try { 100 | // Ensure we're not trying to delete system labels 101 | const label = await gmail.users.labels.get({ 102 | userId: 'me', 103 | id: labelId, 104 | }); 105 | 106 | if (label.data.type === 'system') { 107 | throw new Error(`Cannot delete system label with ID "${labelId}".`); 108 | } 109 | 110 | await gmail.users.labels.delete({ 111 | userId: 'me', 112 | id: labelId, 113 | }); 114 | 115 | return { success: true, message: `Label "${label.data.name}" deleted successfully.` }; 116 | } catch (error: any) { 117 | if (error.code === 404) { 118 | throw new Error(`Label with ID "${labelId}" not found.`); 119 | } 120 | 121 | throw new Error(`Failed to delete label: ${error.message}`); 122 | } 123 | } 124 | 125 | /** 126 | * Gets a detailed list of all Gmail labels 127 | * @param gmail - Gmail API instance 128 | * @returns Object containing system and user labels 129 | */ 130 | export async function listLabels(gmail: any) { 131 | try { 132 | const response = await gmail.users.labels.list({ 133 | userId: 'me', 134 | }); 135 | 136 | const labels = response.data.labels || []; 137 | 138 | // Group labels by type for better organization 139 | const systemLabels = labels.filter((label:GmailLabel) => label.type === 'system'); 140 | const userLabels = labels.filter((label:GmailLabel) => label.type === 'user'); 141 | 142 | return { 143 | all: labels, 144 | system: systemLabels, 145 | user: userLabels, 146 | count: { 147 | total: labels.length, 148 | system: systemLabels.length, 149 | user: userLabels.length 150 | } 151 | }; 152 | } catch (error: any) { 153 | throw new Error(`Failed to list labels: ${error.message}`); 154 | } 155 | } 156 | 157 | /** 158 | * Finds a label by name 159 | * @param gmail - Gmail API instance 160 | * @param labelName - Name of the label to find 161 | * @returns The found label or null if not found 162 | */ 163 | export async function findLabelByName(gmail: any, labelName: string) { 164 | try { 165 | const labelsResponse = await listLabels(gmail); 166 | const allLabels = labelsResponse.all; 167 | 168 | // Case-insensitive match 169 | const foundLabel = allLabels.find( 170 | (label: GmailLabel) => label.name.toLowerCase() === labelName.toLowerCase() 171 | ); 172 | 173 | return foundLabel || null; 174 | } catch (error: any) { 175 | throw new Error(`Failed to find label: ${error.message}`); 176 | } 177 | } 178 | 179 | /** 180 | * Creates label if it doesn't exist or returns existing label 181 | * @param gmail - Gmail API instance 182 | * @param labelName - Name of the label to create 183 | * @param options - Optional settings for the label 184 | * @returns The new or existing label 185 | */ 186 | export async function getOrCreateLabel(gmail: any, labelName: string, options: { 187 | messageListVisibility?: string; 188 | labelListVisibility?: string; 189 | } = {}) { 190 | try { 191 | // First try to find an existing label 192 | const existingLabel = await findLabelByName(gmail, labelName); 193 | 194 | if (existingLabel) { 195 | return existingLabel; 196 | } 197 | 198 | // If not found, create a new one 199 | return await createLabel(gmail, labelName, options); 200 | } catch (error: any) { 201 | throw new Error(`Failed to get or create label: ${error.message}`); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gmail AutoAuth MCP Server 2 | 3 | A Model Context Protocol (MCP) server for Gmail integration in Claude Desktop with auto authentication support. This server enables AI assistants to manage Gmail through natural language interactions. 4 | 5 | ![](https://badge.mcpx.dev?type=server 'MCP Server') 6 | [![smithery badge](https://smithery.ai/badge/@gongrzhe/server-gmail-autoauth-mcp)](https://smithery.ai/server/@gongrzhe/server-gmail-autoauth-mcp) 7 | 8 | 9 | ## Features 10 | 11 | - Send emails with subject, content, **attachments**, and recipients 12 | - **Full attachment support** - send and receive file attachments 13 | - **Download email attachments** to local filesystem 14 | - Support for HTML emails and multipart messages with both HTML and plain text versions 15 | - Full support for international characters in subject lines and email content 16 | - Read email messages by ID with advanced MIME structure handling 17 | - **Enhanced attachment display** showing filenames, types, sizes, and download IDs 18 | - Search emails with various criteria (subject, sender, date range) 19 | - **Comprehensive label management with ability to create, update, delete and list labels** 20 | - List all available Gmail labels (system and user-defined) 21 | - List emails in inbox, sent, or custom labels 22 | - Mark emails as read/unread 23 | - Move emails to different labels/folders 24 | - Delete emails 25 | - **Batch operations for efficiently processing multiple emails at once** 26 | - Full integration with Gmail API 27 | - Simple OAuth2 authentication flow with auto browser launch 28 | - Support for both Desktop and Web application credentials 29 | - Global credential storage for convenience 30 | 31 | ## Installation & Authentication 32 | 33 | ### Installing via Smithery 34 | 35 | To install Gmail AutoAuth for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@gongrzhe/server-gmail-autoauth-mcp): 36 | 37 | ```bash 38 | npx -y @smithery/cli install @gongrzhe/server-gmail-autoauth-mcp --client claude 39 | ``` 40 | 41 | ### Installing Manually 42 | 1. Create a Google Cloud Project and obtain credentials: 43 | 44 | a. Create a Google Cloud Project: 45 | - Go to [Google Cloud Console](https://console.cloud.google.com/) 46 | - Create a new project or select an existing one 47 | - Enable the Gmail API for your project 48 | 49 | b. Create OAuth 2.0 Credentials: 50 | - Go to "APIs & Services" > "Credentials" 51 | - Click "Create Credentials" > "OAuth client ID" 52 | - Choose either "Desktop app" or "Web application" as application type 53 | - Give it a name and click "Create" 54 | - For Web application, add `http://localhost:3000/oauth2callback` to the authorized redirect URIs 55 | - Download the JSON file of your client's OAuth keys 56 | - Rename the key file to `gcp-oauth.keys.json` 57 | 58 | 2. Run Authentication: 59 | 60 | You can authenticate in two ways: 61 | 62 | a. Global Authentication (Recommended): 63 | ```bash 64 | # First time: Place gcp-oauth.keys.json in your home directory's .gmail-mcp folder 65 | mkdir -p ~/.gmail-mcp 66 | mv gcp-oauth.keys.json ~/.gmail-mcp/ 67 | 68 | # Run authentication from anywhere 69 | npx @gongrzhe/server-gmail-autoauth-mcp auth 70 | ``` 71 | 72 | b. Local Authentication: 73 | ```bash 74 | # Place gcp-oauth.keys.json in your current directory 75 | # The file will be automatically copied to global config 76 | npx @gongrzhe/server-gmail-autoauth-mcp auth 77 | ``` 78 | 79 | The authentication process will: 80 | - Look for `gcp-oauth.keys.json` in the current directory or `~/.gmail-mcp/` 81 | - If found in current directory, copy it to `~/.gmail-mcp/` 82 | - Open your default browser for Google authentication 83 | - Save credentials as `~/.gmail-mcp/credentials.json` 84 | 85 | > **Note**: 86 | > - After successful authentication, credentials are stored globally in `~/.gmail-mcp/` and can be used from any directory 87 | > - Both Desktop app and Web application credentials are supported 88 | > - For Web application credentials, make sure to add `http://localhost:3000/oauth2callback` to your authorized redirect URIs 89 | 90 | 3. Configure in Claude Desktop: 91 | 92 | ```json 93 | { 94 | "mcpServers": { 95 | "gmail": { 96 | "command": "npx", 97 | "args": [ 98 | "@gongrzhe/server-gmail-autoauth-mcp" 99 | ] 100 | } 101 | } 102 | } 103 | ``` 104 | 105 | ### Docker Support 106 | 107 | If you prefer using Docker: 108 | 109 | 1. Authentication: 110 | ```bash 111 | docker run -i --rm \ 112 | --mount type=bind,source=/path/to/gcp-oauth.keys.json,target=/gcp-oauth.keys.json \ 113 | -v mcp-gmail:/gmail-server \ 114 | -e GMAIL_OAUTH_PATH=/gcp-oauth.keys.json \ 115 | -e "GMAIL_CREDENTIALS_PATH=/gmail-server/credentials.json" \ 116 | -p 3000:3000 \ 117 | mcp/gmail auth 118 | ``` 119 | 120 | 2. Usage: 121 | ```json 122 | { 123 | "mcpServers": { 124 | "gmail": { 125 | "command": "docker", 126 | "args": [ 127 | "run", 128 | "-i", 129 | "--rm", 130 | "-v", 131 | "mcp-gmail:/gmail-server", 132 | "-e", 133 | "GMAIL_CREDENTIALS_PATH=/gmail-server/credentials.json", 134 | "mcp/gmail" 135 | ] 136 | } 137 | } 138 | } 139 | ``` 140 | 141 | ### Cloud Server Authentication 142 | 143 | For cloud server environments (like n8n), you can specify a custom callback URL during authentication: 144 | 145 | ```bash 146 | npx @gongrzhe/server-gmail-autoauth-mcp auth https://gmail.gongrzhe.com/oauth2callback 147 | ``` 148 | 149 | #### Setup Instructions for Cloud Environment 150 | 151 | 1. **Configure Reverse Proxy:** 152 | - Set up your n8n container to expose a port for authentication 153 | - Configure a reverse proxy to forward traffic from your domain (e.g., `gmail.gongrzhe.com`) to this port 154 | 155 | 2. **DNS Configuration:** 156 | - Add an A record in your DNS settings to resolve your domain to your cloud server's IP address 157 | 158 | 3. **Google Cloud Platform Setup:** 159 | - In your Google Cloud Console, add your custom domain callback URL (e.g., `https://gmail.gongrzhe.com/oauth2callback`) to the authorized redirect URIs list 160 | 161 | 4. **Run Authentication:** 162 | ```bash 163 | npx @gongrzhe/server-gmail-autoauth-mcp auth https://gmail.gongrzhe.com/oauth2callback 164 | ``` 165 | 166 | 5. **Configure in your application:** 167 | ```json 168 | { 169 | "mcpServers": { 170 | "gmail": { 171 | "command": "npx", 172 | "args": [ 173 | "@gongrzhe/server-gmail-autoauth-mcp" 174 | ] 175 | } 176 | } 177 | } 178 | ``` 179 | 180 | This approach allows authentication flows to work properly in environments where localhost isn't accessible, such as containerized applications or cloud servers. 181 | 182 | ## Available Tools 183 | 184 | The server provides the following tools that can be used through Claude Desktop: 185 | 186 | ### 1. Send Email (`send_email`) 187 | 188 | Sends a new email immediately. Supports plain text, HTML, or multipart emails **with optional file attachments**. 189 | 190 | Basic Email: 191 | ```json 192 | { 193 | "to": ["recipient@example.com"], 194 | "subject": "Meeting Tomorrow", 195 | "body": "Hi,\n\nJust a reminder about our meeting tomorrow at 10 AM.\n\nBest regards", 196 | "cc": ["cc@example.com"], 197 | "bcc": ["bcc@example.com"], 198 | "mimeType": "text/plain" 199 | } 200 | ``` 201 | 202 | **Email with Attachments:** 203 | ```json 204 | { 205 | "to": ["recipient@example.com"], 206 | "subject": "Project Files", 207 | "body": "Hi,\n\nPlease find the project files attached.\n\nBest regards", 208 | "attachments": [ 209 | "/path/to/document.pdf", 210 | "/path/to/spreadsheet.xlsx", 211 | "/path/to/presentation.pptx" 212 | ] 213 | } 214 | ``` 215 | 216 | HTML Email Example: 217 | ```json 218 | { 219 | "to": ["recipient@example.com"], 220 | "subject": "Meeting Tomorrow", 221 | "mimeType": "text/html", 222 | "body": "

Meeting Reminder

Just a reminder about our meeting tomorrow at 10 AM.

Best regards

" 223 | } 224 | ``` 225 | 226 | Multipart Email Example (HTML + Plain Text): 227 | ```json 228 | { 229 | "to": ["recipient@example.com"], 230 | "subject": "Meeting Tomorrow", 231 | "mimeType": "multipart/alternative", 232 | "body": "Hi,\n\nJust a reminder about our meeting tomorrow at 10 AM.\n\nBest regards", 233 | "htmlBody": "

Meeting Reminder

Just a reminder about our meeting tomorrow at 10 AM.

Best regards

" 234 | } 235 | ``` 236 | 237 | ### 2. Draft Email (`draft_email`) 238 | Creates a draft email without sending it. **Also supports attachments**. 239 | 240 | ```json 241 | { 242 | "to": ["recipient@example.com"], 243 | "subject": "Draft Report", 244 | "body": "Here's the draft report for your review.", 245 | "cc": ["manager@example.com"], 246 | "attachments": ["/path/to/draft_report.docx"] 247 | } 248 | ``` 249 | 250 | ### 3. Read Email (`read_email`) 251 | Retrieves the content of a specific email by its ID. **Now shows enhanced attachment information**. 252 | 253 | ```json 254 | { 255 | "messageId": "182ab45cd67ef" 256 | } 257 | ``` 258 | 259 | **Enhanced Response includes attachment details:** 260 | ``` 261 | Subject: Project Files 262 | From: sender@example.com 263 | To: recipient@example.com 264 | Date: Thu, 19 Jun 2025 10:30:00 -0400 265 | 266 | Email body content here... 267 | 268 | Attachments (2): 269 | - document.pdf (application/pdf, 245 KB, ID: ANGjdJ9fkTs-i3GCQo5o97f_itG...) 270 | - spreadsheet.xlsx (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, 89 KB, ID: BWHkeL8gkUt-j4HDRp6o98g_juI...) 271 | ``` 272 | 273 | ### 4. **Download Attachment (`download_attachment`)** 274 | **NEW**: Downloads email attachments to your local filesystem. 275 | 276 | ```json 277 | { 278 | "messageId": "182ab45cd67ef", 279 | "attachmentId": "ANGjdJ9fkTs-i3GCQo5o97f_itG...", 280 | "savePath": "/path/to/downloads", 281 | "filename": "downloaded_document.pdf" 282 | } 283 | ``` 284 | 285 | Parameters: 286 | - `messageId`: The ID of the email containing the attachment 287 | - `attachmentId`: The attachment ID (shown in enhanced email display) 288 | - `savePath`: Directory to save the file (optional, defaults to current directory) 289 | - `filename`: Custom filename (optional, uses original filename if not provided) 290 | 291 | ### 5. Search Emails (`search_emails`) 292 | Searches for emails using Gmail search syntax. 293 | 294 | ```json 295 | { 296 | "query": "from:sender@example.com after:2024/01/01 has:attachment", 297 | "maxResults": 10 298 | } 299 | ``` 300 | 301 | ### 6. Modify Email (`modify_email`) 302 | Adds or removes labels from emails (move to different folders, archive, etc.). 303 | 304 | ```json 305 | { 306 | "messageId": "182ab45cd67ef", 307 | "addLabelIds": ["IMPORTANT"], 308 | "removeLabelIds": ["INBOX"] 309 | } 310 | ``` 311 | 312 | ### 7. Delete Email (`delete_email`) 313 | Permanently deletes an email. 314 | 315 | ```json 316 | { 317 | "messageId": "182ab45cd67ef" 318 | } 319 | ``` 320 | 321 | ### 8. List Email Labels (`list_email_labels`) 322 | Retrieves all available Gmail labels. 323 | 324 | ```json 325 | {} 326 | ``` 327 | 328 | ### 9. Create Label (`create_label`) 329 | Creates a new Gmail label. 330 | 331 | ```json 332 | { 333 | "name": "Important Projects", 334 | "messageListVisibility": "show", 335 | "labelListVisibility": "labelShow" 336 | } 337 | ``` 338 | 339 | ### 10. Update Label (`update_label`) 340 | Updates an existing Gmail label. 341 | 342 | ```json 343 | { 344 | "id": "Label_1234567890", 345 | "name": "Urgent Projects", 346 | "messageListVisibility": "show", 347 | "labelListVisibility": "labelShow" 348 | } 349 | ``` 350 | 351 | ### 11. Delete Label (`delete_label`) 352 | Deletes a Gmail label. 353 | 354 | ```json 355 | { 356 | "id": "Label_1234567890" 357 | } 358 | ``` 359 | 360 | ### 12. Get or Create Label (`get_or_create_label`) 361 | Gets an existing label by name or creates it if it doesn't exist. 362 | 363 | ```json 364 | { 365 | "name": "Project XYZ", 366 | "messageListVisibility": "show", 367 | "labelListVisibility": "labelShow" 368 | } 369 | ``` 370 | 371 | ### 13. Batch Modify Emails (`batch_modify_emails`) 372 | Modifies labels for multiple emails in efficient batches. 373 | 374 | ```json 375 | { 376 | "messageIds": ["182ab45cd67ef", "182ab45cd67eg", "182ab45cd67eh"], 377 | "addLabelIds": ["IMPORTANT"], 378 | "removeLabelIds": ["INBOX"], 379 | "batchSize": 50 380 | } 381 | ``` 382 | 383 | ### 14. Batch Delete Emails (`batch_delete_emails`) 384 | Permanently deletes multiple emails in efficient batches. 385 | 386 | ```json 387 | { 388 | "messageIds": ["182ab45cd67ef", "182ab45cd67eg", "182ab45cd67eh"], 389 | "batchSize": 50 390 | } 391 | ``` 392 | 393 | ### 14. Create Filter (`create_filter`) 394 | Creates a new Gmail filter with custom criteria and actions. 395 | 396 | ```json 397 | { 398 | "criteria": { 399 | "from": "newsletter@company.com", 400 | "hasAttachment": false 401 | }, 402 | "action": { 403 | "addLabelIds": ["Label_Newsletter"], 404 | "removeLabelIds": ["INBOX"] 405 | } 406 | } 407 | ``` 408 | 409 | ### 15. List Filters (`list_filters`) 410 | Retrieves all Gmail filters. 411 | 412 | ```json 413 | {} 414 | ``` 415 | 416 | ### 16. Get Filter (`get_filter`) 417 | Gets details of a specific Gmail filter. 418 | 419 | ```json 420 | { 421 | "filterId": "ANe1Bmj1234567890" 422 | } 423 | ``` 424 | 425 | ### 17. Delete Filter (`delete_filter`) 426 | Deletes a Gmail filter. 427 | 428 | ```json 429 | { 430 | "filterId": "ANe1Bmj1234567890" 431 | } 432 | ``` 433 | 434 | ### 18. Create Filter from Template (`create_filter_from_template`) 435 | Creates a filter using pre-defined templates for common scenarios. 436 | 437 | ```json 438 | { 439 | "template": "fromSender", 440 | "parameters": { 441 | "senderEmail": "notifications@github.com", 442 | "labelIds": ["Label_GitHub"], 443 | "archive": true 444 | } 445 | } 446 | ``` 447 | 448 | ## Filter Management Features 449 | 450 | ### Filter Criteria 451 | 452 | You can create filters based on various criteria: 453 | 454 | | Criteria | Example | Description | 455 | |----------|---------|-------------| 456 | | `from` | `"sender@example.com"` | Emails from a specific sender | 457 | | `to` | `"recipient@example.com"` | Emails sent to a specific recipient | 458 | | `subject` | `"Meeting"` | Emails with specific text in subject | 459 | | `query` | `"has:attachment"` | Gmail search query syntax | 460 | | `negatedQuery` | `"spam"` | Text that must NOT be present | 461 | | `hasAttachment` | `true` | Emails with attachments | 462 | | `size` | `10485760` | Email size in bytes | 463 | | `sizeComparison` | `"larger"` | Size comparison (`larger`, `smaller`) | 464 | 465 | ### Filter Actions 466 | 467 | Filters can perform the following actions: 468 | 469 | | Action | Example | Description | 470 | |--------|---------|-------------| 471 | | `addLabelIds` | `["IMPORTANT", "Label_Work"]` | Add labels to matching emails | 472 | | `removeLabelIds` | `["INBOX", "UNREAD"]` | Remove labels from matching emails | 473 | | `forward` | `"backup@example.com"` | Forward emails to another address | 474 | 475 | ### Filter Templates 476 | 477 | The server includes pre-built templates for common filtering scenarios: 478 | 479 | #### 1. From Sender Template (`fromSender`) 480 | Filters emails from a specific sender and optionally archives them. 481 | 482 | ```json 483 | { 484 | "template": "fromSender", 485 | "parameters": { 486 | "senderEmail": "newsletter@company.com", 487 | "labelIds": ["Label_Newsletter"], 488 | "archive": true 489 | } 490 | } 491 | ``` 492 | 493 | #### 2. Subject Filter Template (`withSubject`) 494 | Filters emails with specific subject text and optionally marks as read. 495 | 496 | ```json 497 | { 498 | "template": "withSubject", 499 | "parameters": { 500 | "subjectText": "[URGENT]", 501 | "labelIds": ["Label_Urgent"], 502 | "markAsRead": false 503 | } 504 | } 505 | ``` 506 | 507 | #### 3. Attachment Filter Template (`withAttachments`) 508 | Filters all emails with attachments. 509 | 510 | ```json 511 | { 512 | "template": "withAttachments", 513 | "parameters": { 514 | "labelIds": ["Label_Attachments"] 515 | } 516 | } 517 | ``` 518 | 519 | #### 4. Large Email Template (`largeEmails`) 520 | Filters emails larger than a specified size. 521 | 522 | ```json 523 | { 524 | "template": "largeEmails", 525 | "parameters": { 526 | "sizeInBytes": 10485760, 527 | "labelIds": ["Label_Large"] 528 | } 529 | } 530 | ``` 531 | 532 | #### 5. Content Filter Template (`containingText`) 533 | Filters emails containing specific text and optionally marks as important. 534 | 535 | ```json 536 | { 537 | "template": "containingText", 538 | "parameters": { 539 | "searchText": "invoice", 540 | "labelIds": ["Label_Finance"], 541 | "markImportant": true 542 | } 543 | } 544 | ``` 545 | 546 | #### 6. Mailing List Template (`mailingList`) 547 | Filters mailing list emails and optionally archives them. 548 | 549 | ```json 550 | { 551 | "template": "mailingList", 552 | "parameters": { 553 | "listIdentifier": "dev-team", 554 | "labelIds": ["Label_DevTeam"], 555 | "archive": true 556 | } 557 | } 558 | ``` 559 | 560 | ### Common Filter Examples 561 | 562 | Here are some practical filter examples: 563 | 564 | **Auto-organize newsletters:** 565 | ```json 566 | { 567 | "criteria": { 568 | "from": "newsletter@company.com" 569 | }, 570 | "action": { 571 | "addLabelIds": ["Label_Newsletter"], 572 | "removeLabelIds": ["INBOX"] 573 | } 574 | } 575 | ``` 576 | 577 | **Handle promotional emails:** 578 | ```json 579 | { 580 | "criteria": { 581 | "query": "unsubscribe OR promotional" 582 | }, 583 | "action": { 584 | "addLabelIds": ["Label_Promotions"], 585 | "removeLabelIds": ["INBOX", "UNREAD"] 586 | } 587 | } 588 | ``` 589 | 590 | **Priority emails from boss:** 591 | ```json 592 | { 593 | "criteria": { 594 | "from": "boss@company.com" 595 | }, 596 | "action": { 597 | "addLabelIds": ["IMPORTANT", "Label_Boss"] 598 | } 599 | } 600 | ``` 601 | 602 | **Large attachments:** 603 | ```json 604 | { 605 | "criteria": { 606 | "size": 10485760, 607 | "sizeComparison": "larger", 608 | "hasAttachment": true 609 | }, 610 | "action": { 611 | "addLabelIds": ["Label_LargeFiles"] 612 | } 613 | } 614 | ``` 615 | 616 | ## Advanced Search Syntax 617 | 618 | The `search_emails` tool supports Gmail's powerful search operators: 619 | 620 | | Operator | Example | Description | 621 | |----------|---------|-------------| 622 | | `from:` | `from:john@example.com` | Emails from a specific sender | 623 | | `to:` | `to:mary@example.com` | Emails sent to a specific recipient | 624 | | `subject:` | `subject:"meeting notes"` | Emails with specific text in the subject | 625 | | `has:attachment` | `has:attachment` | Emails with attachments | 626 | | `after:` | `after:2024/01/01` | Emails received after a date | 627 | | `before:` | `before:2024/02/01` | Emails received before a date | 628 | | `is:` | `is:unread` | Emails with a specific state | 629 | | `label:` | `label:work` | Emails with a specific label | 630 | 631 | You can combine multiple operators: `from:john@example.com after:2024/01/01 has:attachment` 632 | 633 | ## Advanced Features 634 | 635 | ### **Email Attachment Support** 636 | 637 | The server provides comprehensive attachment functionality: 638 | 639 | - **Sending Attachments**: Include file paths in the `attachments` array when sending or drafting emails 640 | - **Attachment Detection**: Automatically detects MIME types and file sizes 641 | - **Download Capability**: Download any email attachment to your local filesystem 642 | - **Enhanced Display**: View detailed attachment information including filenames, types, sizes, and download IDs 643 | - **Multiple Formats**: Support for all common file types (documents, images, archives, etc.) 644 | - **RFC822 Compliance**: Uses Nodemailer for proper MIME message formatting 645 | 646 | **Supported File Types**: All standard file types including PDF, DOCX, XLSX, PPTX, images (PNG, JPG, GIF), archives (ZIP, RAR), and more. 647 | 648 | ### Email Content Extraction 649 | 650 | The server intelligently extracts email content from complex MIME structures: 651 | 652 | - Prioritizes plain text content when available 653 | - Falls back to HTML content if plain text is not available 654 | - Handles multi-part MIME messages with nested parts 655 | - **Processes attachments information (filename, type, size, download ID)** 656 | - Preserves original email headers (From, To, Subject, Date) 657 | 658 | ### International Character Support 659 | 660 | The server fully supports non-ASCII characters in email subjects and content, including: 661 | - Turkish, Chinese, Japanese, Korean, and other non-Latin alphabets 662 | - Special characters and symbols 663 | - Proper encoding ensures correct display in email clients 664 | 665 | ### Comprehensive Label Management 666 | 667 | The server provides a complete set of tools for managing Gmail labels: 668 | 669 | - **Create Labels**: Create new labels with customizable visibility settings 670 | - **Update Labels**: Rename labels or change their visibility settings 671 | - **Delete Labels**: Remove user-created labels (system labels are protected) 672 | - **Find or Create**: Get a label by name or automatically create it if not found 673 | - **List All Labels**: View all system and user labels with detailed information 674 | - **Label Visibility Options**: Control how labels appear in message and label lists 675 | 676 | Label visibility settings include: 677 | - `messageListVisibility`: Controls whether the label appears in the message list (`show` or `hide`) 678 | - `labelListVisibility`: Controls how the label appears in the label list (`labelShow`, `labelShowIfUnread`, or `labelHide`) 679 | 680 | These label management features enable sophisticated organization of emails directly through Claude, without needing to switch to the Gmail interface. 681 | 682 | ### Batch Operations 683 | 684 | The server includes efficient batch processing capabilities: 685 | 686 | - Process up to 50 emails at once (configurable batch size) 687 | - Automatic chunking of large email sets to avoid API limits 688 | - Detailed success/failure reporting for each operation 689 | - Graceful error handling with individual retries 690 | - Perfect for bulk inbox management and organization tasks 691 | 692 | ## Security Notes 693 | 694 | - OAuth credentials are stored securely in your local environment (`~/.gmail-mcp/`) 695 | - The server uses offline access to maintain persistent authentication 696 | - Never share or commit your credentials to version control 697 | - Regularly review and revoke unused access in your Google Account settings 698 | - Credentials are stored globally but are only accessible by the current user 699 | - **Attachment files are processed locally and never stored permanently by the server** 700 | 701 | ## Troubleshooting 702 | 703 | 1. **OAuth Keys Not Found** 704 | - Make sure `gcp-oauth.keys.json` is in either your current directory or `~/.gmail-mcp/` 705 | - Check file permissions 706 | 707 | 2. **Invalid Credentials Format** 708 | - Ensure your OAuth keys file contains either `web` or `installed` credentials 709 | - For web applications, verify the redirect URI is correctly configured 710 | 711 | 3. **Port Already in Use** 712 | - If port 3000 is already in use, please free it up before running authentication 713 | - You can find and stop the process using that port 714 | 715 | 4. **Batch Operation Failures** 716 | - If batch operations fail, they automatically retry individual items 717 | - Check the detailed error messages for specific failures 718 | - Consider reducing the batch size if you encounter rate limiting 719 | 720 | 5. **Attachment Issues** 721 | - **File Not Found**: Ensure attachment file paths are correct and accessible 722 | - **Permission Errors**: Check that the server has read access to attachment files 723 | - **Size Limits**: Gmail has a 25MB attachment size limit per email 724 | - **Download Failures**: Verify you have write permissions to the download directory 725 | 726 | ## Contributing 727 | 728 | Contributions are welcome! Please feel free to submit a Pull Request. 729 | 730 | 731 | ## Running evals 732 | 733 | The evals package loads an mcp client that then runs the index.ts file, so there is no need to rebuild between tests. You can load environment variables by prefixing the npx command. Full documentation can be found [here](https://www.mcpevals.io/docs). 734 | 735 | ```bash 736 | OPENAI_API_KEY=your-key npx mcp-eval src/evals/evals.ts src/index.ts 737 | ``` 738 | 739 | ## License 740 | 741 | MIT 742 | 743 | ## Support 744 | 745 | If you encounter any issues or have questions, please file an issue on the GitHub repository. 746 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | } from "@modelcontextprotocol/sdk/types.js"; 9 | import { google } from 'googleapis'; 10 | import { z } from "zod"; 11 | import { zodToJsonSchema } from "zod-to-json-schema"; 12 | import { OAuth2Client } from 'google-auth-library'; 13 | import fs from 'fs'; 14 | import path from 'path'; 15 | import { fileURLToPath } from 'url'; 16 | import http from 'http'; 17 | import open from 'open'; 18 | import os from 'os'; 19 | import {createEmailMessage, createEmailWithNodemailer} from "./utl.js"; 20 | import { createLabel, updateLabel, deleteLabel, listLabels, findLabelByName, getOrCreateLabel, GmailLabel } from "./label-manager.js"; 21 | import { createFilter, listFilters, getFilter, deleteFilter, filterTemplates, GmailFilterCriteria, GmailFilterAction } from "./filter-manager.js"; 22 | 23 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 24 | 25 | // Configuration paths 26 | const CONFIG_DIR = path.join(os.homedir(), '.gmail-mcp'); 27 | const OAUTH_PATH = process.env.GMAIL_OAUTH_PATH || path.join(CONFIG_DIR, 'gcp-oauth.keys.json'); 28 | const CREDENTIALS_PATH = process.env.GMAIL_CREDENTIALS_PATH || path.join(CONFIG_DIR, 'credentials.json'); 29 | 30 | // Type definitions for Gmail API responses 31 | interface GmailMessagePart { 32 | partId?: string; 33 | mimeType?: string; 34 | filename?: string; 35 | headers?: Array<{ 36 | name: string; 37 | value: string; 38 | }>; 39 | body?: { 40 | attachmentId?: string; 41 | size?: number; 42 | data?: string; 43 | }; 44 | parts?: GmailMessagePart[]; 45 | } 46 | 47 | interface EmailAttachment { 48 | id: string; 49 | filename: string; 50 | mimeType: string; 51 | size: number; 52 | } 53 | 54 | interface EmailContent { 55 | text: string; 56 | html: string; 57 | } 58 | 59 | // OAuth2 configuration 60 | let oauth2Client: OAuth2Client; 61 | 62 | /** 63 | * Recursively extract email body content from MIME message parts 64 | * Handles complex email structures with nested parts 65 | */ 66 | function extractEmailContent(messagePart: GmailMessagePart): EmailContent { 67 | // Initialize containers for different content types 68 | let textContent = ''; 69 | let htmlContent = ''; 70 | 71 | // If the part has a body with data, process it based on MIME type 72 | if (messagePart.body && messagePart.body.data) { 73 | const content = Buffer.from(messagePart.body.data, 'base64').toString('utf8'); 74 | 75 | // Store content based on its MIME type 76 | if (messagePart.mimeType === 'text/plain') { 77 | textContent = content; 78 | } else if (messagePart.mimeType === 'text/html') { 79 | htmlContent = content; 80 | } 81 | } 82 | 83 | // If the part has nested parts, recursively process them 84 | if (messagePart.parts && messagePart.parts.length > 0) { 85 | for (const part of messagePart.parts) { 86 | const { text, html } = extractEmailContent(part); 87 | if (text) textContent += text; 88 | if (html) htmlContent += html; 89 | } 90 | } 91 | 92 | // Return both plain text and HTML content 93 | return { text: textContent, html: htmlContent }; 94 | } 95 | 96 | async function loadCredentials() { 97 | try { 98 | // Create config directory if it doesn't exist 99 | if (!process.env.GMAIL_OAUTH_PATH && !CREDENTIALS_PATH &&!fs.existsSync(CONFIG_DIR)) { 100 | fs.mkdirSync(CONFIG_DIR, { recursive: true }); 101 | } 102 | 103 | // Check for OAuth keys in current directory first, then in config directory 104 | const localOAuthPath = path.join(process.cwd(), 'gcp-oauth.keys.json'); 105 | let oauthPath = OAUTH_PATH; 106 | 107 | if (fs.existsSync(localOAuthPath)) { 108 | // If found in current directory, copy to config directory 109 | fs.copyFileSync(localOAuthPath, OAUTH_PATH); 110 | console.log('OAuth keys found in current directory, copied to global config.'); 111 | } 112 | 113 | if (!fs.existsSync(OAUTH_PATH)) { 114 | console.error('Error: OAuth keys file not found. Please place gcp-oauth.keys.json in current directory or', CONFIG_DIR); 115 | process.exit(1); 116 | } 117 | 118 | const keysContent = JSON.parse(fs.readFileSync(OAUTH_PATH, 'utf8')); 119 | const keys = keysContent.installed || keysContent.web; 120 | 121 | if (!keys) { 122 | console.error('Error: Invalid OAuth keys file format. File should contain either "installed" or "web" credentials.'); 123 | process.exit(1); 124 | } 125 | 126 | const callback = process.argv[2] === 'auth' && process.argv[3] 127 | ? process.argv[3] 128 | : "http://localhost:3000/oauth2callback"; 129 | 130 | oauth2Client = new OAuth2Client( 131 | keys.client_id, 132 | keys.client_secret, 133 | callback 134 | ); 135 | 136 | if (fs.existsSync(CREDENTIALS_PATH)) { 137 | const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8')); 138 | oauth2Client.setCredentials(credentials); 139 | } 140 | } catch (error) { 141 | console.error('Error loading credentials:', error); 142 | process.exit(1); 143 | } 144 | } 145 | 146 | async function authenticate() { 147 | const server = http.createServer(); 148 | server.listen(3000); 149 | 150 | return new Promise((resolve, reject) => { 151 | const authUrl = oauth2Client.generateAuthUrl({ 152 | access_type: 'offline', 153 | scope: [ 154 | 'https://www.googleapis.com/auth/gmail.modify', 155 | 'https://www.googleapis.com/auth/gmail.settings.basic' 156 | ], 157 | }); 158 | 159 | console.log('Please visit this URL to authenticate:', authUrl); 160 | open(authUrl); 161 | 162 | server.on('request', async (req, res) => { 163 | if (!req.url?.startsWith('/oauth2callback')) return; 164 | 165 | const url = new URL(req.url, 'http://localhost:3000'); 166 | const code = url.searchParams.get('code'); 167 | 168 | if (!code) { 169 | res.writeHead(400); 170 | res.end('No code provided'); 171 | reject(new Error('No code provided')); 172 | return; 173 | } 174 | 175 | try { 176 | const { tokens } = await oauth2Client.getToken(code); 177 | oauth2Client.setCredentials(tokens); 178 | fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(tokens)); 179 | 180 | res.writeHead(200); 181 | res.end('Authentication successful! You can close this window.'); 182 | server.close(); 183 | resolve(); 184 | } catch (error) { 185 | res.writeHead(500); 186 | res.end('Authentication failed'); 187 | reject(error); 188 | } 189 | }); 190 | }); 191 | } 192 | 193 | // Schema definitions 194 | const SendEmailSchema = z.object({ 195 | to: z.array(z.string()).describe("List of recipient email addresses"), 196 | subject: z.string().describe("Email subject"), 197 | body: z.string().describe("Email body content (used for text/plain or when htmlBody not provided)"), 198 | htmlBody: z.string().optional().describe("HTML version of the email body"), 199 | mimeType: z.enum(['text/plain', 'text/html', 'multipart/alternative']).optional().default('text/plain').describe("Email content type"), 200 | cc: z.array(z.string()).optional().describe("List of CC recipients"), 201 | bcc: z.array(z.string()).optional().describe("List of BCC recipients"), 202 | threadId: z.string().optional().describe("Thread ID to reply to"), 203 | inReplyTo: z.string().optional().describe("Message ID being replied to"), 204 | attachments: z.array(z.string()).optional().describe("List of file paths to attach to the email"), 205 | }); 206 | 207 | const ReadEmailSchema = z.object({ 208 | messageId: z.string().describe("ID of the email message to retrieve"), 209 | }); 210 | 211 | const SearchEmailsSchema = z.object({ 212 | query: z.string().describe("Gmail search query (e.g., 'from:example@gmail.com')"), 213 | maxResults: z.number().optional().describe("Maximum number of results to return"), 214 | }); 215 | 216 | // Updated schema to include removeLabelIds 217 | const ModifyEmailSchema = z.object({ 218 | messageId: z.string().describe("ID of the email message to modify"), 219 | labelIds: z.array(z.string()).optional().describe("List of label IDs to apply"), 220 | addLabelIds: z.array(z.string()).optional().describe("List of label IDs to add to the message"), 221 | removeLabelIds: z.array(z.string()).optional().describe("List of label IDs to remove from the message"), 222 | }); 223 | 224 | const DeleteEmailSchema = z.object({ 225 | messageId: z.string().describe("ID of the email message to delete"), 226 | }); 227 | 228 | // New schema for listing email labels 229 | const ListEmailLabelsSchema = z.object({}).describe("Retrieves all available Gmail labels"); 230 | 231 | // Label management schemas 232 | const CreateLabelSchema = z.object({ 233 | name: z.string().describe("Name for the new label"), 234 | messageListVisibility: z.enum(['show', 'hide']).optional().describe("Whether to show or hide the label in the message list"), 235 | labelListVisibility: z.enum(['labelShow', 'labelShowIfUnread', 'labelHide']).optional().describe("Visibility of the label in the label list"), 236 | }).describe("Creates a new Gmail label"); 237 | 238 | const UpdateLabelSchema = z.object({ 239 | id: z.string().describe("ID of the label to update"), 240 | name: z.string().optional().describe("New name for the label"), 241 | messageListVisibility: z.enum(['show', 'hide']).optional().describe("Whether to show or hide the label in the message list"), 242 | labelListVisibility: z.enum(['labelShow', 'labelShowIfUnread', 'labelHide']).optional().describe("Visibility of the label in the label list"), 243 | }).describe("Updates an existing Gmail label"); 244 | 245 | const DeleteLabelSchema = z.object({ 246 | id: z.string().describe("ID of the label to delete"), 247 | }).describe("Deletes a Gmail label"); 248 | 249 | const GetOrCreateLabelSchema = z.object({ 250 | name: z.string().describe("Name of the label to get or create"), 251 | messageListVisibility: z.enum(['show', 'hide']).optional().describe("Whether to show or hide the label in the message list"), 252 | labelListVisibility: z.enum(['labelShow', 'labelShowIfUnread', 'labelHide']).optional().describe("Visibility of the label in the label list"), 253 | }).describe("Gets an existing label by name or creates it if it doesn't exist"); 254 | 255 | // Schemas for batch operations 256 | const BatchModifyEmailsSchema = z.object({ 257 | messageIds: z.array(z.string()).describe("List of message IDs to modify"), 258 | addLabelIds: z.array(z.string()).optional().describe("List of label IDs to add to all messages"), 259 | removeLabelIds: z.array(z.string()).optional().describe("List of label IDs to remove from all messages"), 260 | batchSize: z.number().optional().default(50).describe("Number of messages to process in each batch (default: 50)"), 261 | }); 262 | 263 | const BatchDeleteEmailsSchema = z.object({ 264 | messageIds: z.array(z.string()).describe("List of message IDs to delete"), 265 | batchSize: z.number().optional().default(50).describe("Number of messages to process in each batch (default: 50)"), 266 | }); 267 | 268 | // Filter management schemas 269 | const CreateFilterSchema = z.object({ 270 | criteria: z.object({ 271 | from: z.string().optional().describe("Sender email address to match"), 272 | to: z.string().optional().describe("Recipient email address to match"), 273 | subject: z.string().optional().describe("Subject text to match"), 274 | query: z.string().optional().describe("Gmail search query (e.g., 'has:attachment')"), 275 | negatedQuery: z.string().optional().describe("Text that must NOT be present"), 276 | hasAttachment: z.boolean().optional().describe("Whether to match emails with attachments"), 277 | excludeChats: z.boolean().optional().describe("Whether to exclude chat messages"), 278 | size: z.number().optional().describe("Email size in bytes"), 279 | sizeComparison: z.enum(['unspecified', 'smaller', 'larger']).optional().describe("Size comparison operator") 280 | }).describe("Criteria for matching emails"), 281 | action: z.object({ 282 | addLabelIds: z.array(z.string()).optional().describe("Label IDs to add to matching emails"), 283 | removeLabelIds: z.array(z.string()).optional().describe("Label IDs to remove from matching emails"), 284 | forward: z.string().optional().describe("Email address to forward matching emails to") 285 | }).describe("Actions to perform on matching emails") 286 | }).describe("Creates a new Gmail filter"); 287 | 288 | const ListFiltersSchema = z.object({}).describe("Retrieves all Gmail filters"); 289 | 290 | const GetFilterSchema = z.object({ 291 | filterId: z.string().describe("ID of the filter to retrieve") 292 | }).describe("Gets details of a specific Gmail filter"); 293 | 294 | const DeleteFilterSchema = z.object({ 295 | filterId: z.string().describe("ID of the filter to delete") 296 | }).describe("Deletes a Gmail filter"); 297 | 298 | const CreateFilterFromTemplateSchema = z.object({ 299 | template: z.enum(['fromSender', 'withSubject', 'withAttachments', 'largeEmails', 'containingText', 'mailingList']).describe("Pre-defined filter template to use"), 300 | parameters: z.object({ 301 | senderEmail: z.string().optional().describe("Sender email (for fromSender template)"), 302 | subjectText: z.string().optional().describe("Subject text (for withSubject template)"), 303 | searchText: z.string().optional().describe("Text to search for (for containingText template)"), 304 | listIdentifier: z.string().optional().describe("Mailing list identifier (for mailingList template)"), 305 | sizeInBytes: z.number().optional().describe("Size threshold in bytes (for largeEmails template)"), 306 | labelIds: z.array(z.string()).optional().describe("Label IDs to apply"), 307 | archive: z.boolean().optional().describe("Whether to archive (skip inbox)"), 308 | markAsRead: z.boolean().optional().describe("Whether to mark as read"), 309 | markImportant: z.boolean().optional().describe("Whether to mark as important") 310 | }).describe("Template-specific parameters") 311 | }).describe("Creates a filter using a pre-defined template"); 312 | 313 | const DownloadAttachmentSchema = z.object({ 314 | messageId: z.string().describe("ID of the email message containing the attachment"), 315 | attachmentId: z.string().describe("ID of the attachment to download"), 316 | filename: z.string().optional().describe("Filename to save the attachment as (if not provided, uses original filename)"), 317 | savePath: z.string().optional().describe("Directory path to save the attachment (defaults to current directory)"), 318 | }); 319 | 320 | 321 | // Main function 322 | async function main() { 323 | await loadCredentials(); 324 | 325 | if (process.argv[2] === 'auth') { 326 | await authenticate(); 327 | console.log('Authentication completed successfully'); 328 | process.exit(0); 329 | } 330 | 331 | // Initialize Gmail API 332 | const gmail = google.gmail({ version: 'v1', auth: oauth2Client }); 333 | 334 | // Server implementation 335 | const server = new Server({ 336 | name: "gmail", 337 | version: "1.0.0", 338 | capabilities: { 339 | tools: {}, 340 | }, 341 | }); 342 | 343 | // Tool handlers 344 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ 345 | tools: [ 346 | { 347 | name: "send_email", 348 | description: "Sends a new email", 349 | inputSchema: zodToJsonSchema(SendEmailSchema), 350 | }, 351 | { 352 | name: "draft_email", 353 | description: "Draft a new email", 354 | inputSchema: zodToJsonSchema(SendEmailSchema), 355 | }, 356 | { 357 | name: "read_email", 358 | description: "Retrieves the content of a specific email", 359 | inputSchema: zodToJsonSchema(ReadEmailSchema), 360 | }, 361 | { 362 | name: "search_emails", 363 | description: "Searches for emails using Gmail search syntax", 364 | inputSchema: zodToJsonSchema(SearchEmailsSchema), 365 | }, 366 | { 367 | name: "modify_email", 368 | description: "Modifies email labels (move to different folders)", 369 | inputSchema: zodToJsonSchema(ModifyEmailSchema), 370 | }, 371 | { 372 | name: "delete_email", 373 | description: "Permanently deletes an email", 374 | inputSchema: zodToJsonSchema(DeleteEmailSchema), 375 | }, 376 | { 377 | name: "list_email_labels", 378 | description: "Retrieves all available Gmail labels", 379 | inputSchema: zodToJsonSchema(ListEmailLabelsSchema), 380 | }, 381 | { 382 | name: "batch_modify_emails", 383 | description: "Modifies labels for multiple emails in batches", 384 | inputSchema: zodToJsonSchema(BatchModifyEmailsSchema), 385 | }, 386 | { 387 | name: "batch_delete_emails", 388 | description: "Permanently deletes multiple emails in batches", 389 | inputSchema: zodToJsonSchema(BatchDeleteEmailsSchema), 390 | }, 391 | { 392 | name: "create_label", 393 | description: "Creates a new Gmail label", 394 | inputSchema: zodToJsonSchema(CreateLabelSchema), 395 | }, 396 | { 397 | name: "update_label", 398 | description: "Updates an existing Gmail label", 399 | inputSchema: zodToJsonSchema(UpdateLabelSchema), 400 | }, 401 | { 402 | name: "delete_label", 403 | description: "Deletes a Gmail label", 404 | inputSchema: zodToJsonSchema(DeleteLabelSchema), 405 | }, 406 | { 407 | name: "get_or_create_label", 408 | description: "Gets an existing label by name or creates it if it doesn't exist", 409 | inputSchema: zodToJsonSchema(GetOrCreateLabelSchema), 410 | }, 411 | { 412 | name: "create_filter", 413 | description: "Creates a new Gmail filter with custom criteria and actions", 414 | inputSchema: zodToJsonSchema(CreateFilterSchema), 415 | }, 416 | { 417 | name: "list_filters", 418 | description: "Retrieves all Gmail filters", 419 | inputSchema: zodToJsonSchema(ListFiltersSchema), 420 | }, 421 | { 422 | name: "get_filter", 423 | description: "Gets details of a specific Gmail filter", 424 | inputSchema: zodToJsonSchema(GetFilterSchema), 425 | }, 426 | { 427 | name: "delete_filter", 428 | description: "Deletes a Gmail filter", 429 | inputSchema: zodToJsonSchema(DeleteFilterSchema), 430 | }, 431 | { 432 | name: "create_filter_from_template", 433 | description: "Creates a filter using a pre-defined template for common scenarios", 434 | inputSchema: zodToJsonSchema(CreateFilterFromTemplateSchema), 435 | }, 436 | { 437 | name: "download_attachment", 438 | description: "Downloads an email attachment to a specified location", 439 | inputSchema: zodToJsonSchema(DownloadAttachmentSchema), 440 | }, 441 | ], 442 | })) 443 | 444 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 445 | const { name, arguments: args } = request.params; 446 | 447 | async function handleEmailAction(action: "send" | "draft", validatedArgs: any) { 448 | let message: string; 449 | 450 | try { 451 | // Check if we have attachments 452 | if (validatedArgs.attachments && validatedArgs.attachments.length > 0) { 453 | // Use Nodemailer to create properly formatted RFC822 message 454 | message = await createEmailWithNodemailer(validatedArgs); 455 | 456 | if (action === "send") { 457 | const encodedMessage = Buffer.from(message).toString('base64') 458 | .replace(/\+/g, '-') 459 | .replace(/\//g, '_') 460 | .replace(/=+$/, ''); 461 | 462 | const result = await gmail.users.messages.send({ 463 | userId: 'me', 464 | requestBody: { 465 | raw: encodedMessage, 466 | ...(validatedArgs.threadId && { threadId: validatedArgs.threadId }) 467 | } 468 | }); 469 | 470 | return { 471 | content: [ 472 | { 473 | type: "text", 474 | text: `Email sent successfully with ID: ${result.data.id}`, 475 | }, 476 | ], 477 | }; 478 | } else { 479 | // For drafts with attachments, use the raw message 480 | const encodedMessage = Buffer.from(message).toString('base64') 481 | .replace(/\+/g, '-') 482 | .replace(/\//g, '_') 483 | .replace(/=+$/, ''); 484 | 485 | const messageRequest = { 486 | raw: encodedMessage, 487 | ...(validatedArgs.threadId && { threadId: validatedArgs.threadId }) 488 | }; 489 | 490 | const response = await gmail.users.drafts.create({ 491 | userId: 'me', 492 | requestBody: { 493 | message: messageRequest, 494 | }, 495 | }); 496 | return { 497 | content: [ 498 | { 499 | type: "text", 500 | text: `Email draft created successfully with ID: ${response.data.id}`, 501 | }, 502 | ], 503 | }; 504 | } 505 | } else { 506 | // For emails without attachments, use the existing simple method 507 | message = createEmailMessage(validatedArgs); 508 | 509 | const encodedMessage = Buffer.from(message).toString('base64') 510 | .replace(/\+/g, '-') 511 | .replace(/\//g, '_') 512 | .replace(/=+$/, ''); 513 | 514 | // Define the type for messageRequest 515 | interface GmailMessageRequest { 516 | raw: string; 517 | threadId?: string; 518 | } 519 | 520 | const messageRequest: GmailMessageRequest = { 521 | raw: encodedMessage, 522 | }; 523 | 524 | // Add threadId if specified 525 | if (validatedArgs.threadId) { 526 | messageRequest.threadId = validatedArgs.threadId; 527 | } 528 | 529 | if (action === "send") { 530 | const response = await gmail.users.messages.send({ 531 | userId: 'me', 532 | requestBody: messageRequest, 533 | }); 534 | return { 535 | content: [ 536 | { 537 | type: "text", 538 | text: `Email sent successfully with ID: ${response.data.id}`, 539 | }, 540 | ], 541 | }; 542 | } else { 543 | const response = await gmail.users.drafts.create({ 544 | userId: 'me', 545 | requestBody: { 546 | message: messageRequest, 547 | }, 548 | }); 549 | return { 550 | content: [ 551 | { 552 | type: "text", 553 | text: `Email draft created successfully with ID: ${response.data.id}`, 554 | }, 555 | ], 556 | }; 557 | } 558 | } 559 | } catch (error: any) { 560 | // Log attachment-related errors for debugging 561 | if (validatedArgs.attachments && validatedArgs.attachments.length > 0) { 562 | console.error(`Failed to send email with ${validatedArgs.attachments.length} attachments:`, error.message); 563 | } 564 | throw error; 565 | } 566 | } 567 | 568 | // Helper function to process operations in batches 569 | async function processBatches( 570 | items: T[], 571 | batchSize: number, 572 | processFn: (batch: T[]) => Promise 573 | ): Promise<{ successes: U[], failures: { item: T, error: Error }[] }> { 574 | const successes: U[] = []; 575 | const failures: { item: T, error: Error }[] = []; 576 | 577 | // Process in batches 578 | for (let i = 0; i < items.length; i += batchSize) { 579 | const batch = items.slice(i, i + batchSize); 580 | try { 581 | const results = await processFn(batch); 582 | successes.push(...results); 583 | } catch (error) { 584 | // If batch fails, try individual items 585 | for (const item of batch) { 586 | try { 587 | const result = await processFn([item]); 588 | successes.push(...result); 589 | } catch (itemError) { 590 | failures.push({ item, error: itemError as Error }); 591 | } 592 | } 593 | } 594 | } 595 | 596 | return { successes, failures }; 597 | } 598 | 599 | try { 600 | switch (name) { 601 | case "send_email": 602 | case "draft_email": { 603 | const validatedArgs = SendEmailSchema.parse(args); 604 | const action = name === "send_email" ? "send" : "draft"; 605 | return await handleEmailAction(action, validatedArgs); 606 | } 607 | 608 | case "read_email": { 609 | const validatedArgs = ReadEmailSchema.parse(args); 610 | const response = await gmail.users.messages.get({ 611 | userId: 'me', 612 | id: validatedArgs.messageId, 613 | format: 'full', 614 | }); 615 | 616 | const headers = response.data.payload?.headers || []; 617 | const subject = headers.find(h => h.name?.toLowerCase() === 'subject')?.value || ''; 618 | const from = headers.find(h => h.name?.toLowerCase() === 'from')?.value || ''; 619 | const to = headers.find(h => h.name?.toLowerCase() === 'to')?.value || ''; 620 | const date = headers.find(h => h.name?.toLowerCase() === 'date')?.value || ''; 621 | const threadId = response.data.threadId || ''; 622 | 623 | // Extract email content using the recursive function 624 | const { text, html } = extractEmailContent(response.data.payload as GmailMessagePart || {}); 625 | 626 | // Use plain text content if available, otherwise use HTML content 627 | // (optionally, you could implement HTML-to-text conversion here) 628 | let body = text || html || ''; 629 | 630 | // If we only have HTML content, add a note for the user 631 | const contentTypeNote = !text && html ? 632 | '[Note: This email is HTML-formatted. Plain text version not available.]\n\n' : ''; 633 | 634 | // Get attachment information 635 | const attachments: EmailAttachment[] = []; 636 | const processAttachmentParts = (part: GmailMessagePart, path: string = '') => { 637 | if (part.body && part.body.attachmentId) { 638 | const filename = part.filename || `attachment-${part.body.attachmentId}`; 639 | attachments.push({ 640 | id: part.body.attachmentId, 641 | filename: filename, 642 | mimeType: part.mimeType || 'application/octet-stream', 643 | size: part.body.size || 0 644 | }); 645 | } 646 | 647 | if (part.parts) { 648 | part.parts.forEach((subpart: GmailMessagePart) => 649 | processAttachmentParts(subpart, `${path}/parts`) 650 | ); 651 | } 652 | }; 653 | 654 | if (response.data.payload) { 655 | processAttachmentParts(response.data.payload as GmailMessagePart); 656 | } 657 | 658 | // Add attachment info to output if any are present 659 | const attachmentInfo = attachments.length > 0 ? 660 | `\n\nAttachments (${attachments.length}):\n` + 661 | attachments.map(a => `- ${a.filename} (${a.mimeType}, ${Math.round(a.size/1024)} KB, ID: ${a.id})`).join('\n') : ''; 662 | 663 | return { 664 | content: [ 665 | { 666 | type: "text", 667 | text: `Thread ID: ${threadId}\nSubject: ${subject}\nFrom: ${from}\nTo: ${to}\nDate: ${date}\n\n${contentTypeNote}${body}${attachmentInfo}`, 668 | }, 669 | ], 670 | }; 671 | } 672 | 673 | case "search_emails": { 674 | const validatedArgs = SearchEmailsSchema.parse(args); 675 | const response = await gmail.users.messages.list({ 676 | userId: 'me', 677 | q: validatedArgs.query, 678 | maxResults: validatedArgs.maxResults || 10, 679 | }); 680 | 681 | const messages = response.data.messages || []; 682 | const results = await Promise.all( 683 | messages.map(async (msg) => { 684 | const detail = await gmail.users.messages.get({ 685 | userId: 'me', 686 | id: msg.id!, 687 | format: 'metadata', 688 | metadataHeaders: ['Subject', 'From', 'Date'], 689 | }); 690 | const headers = detail.data.payload?.headers || []; 691 | return { 692 | id: msg.id, 693 | subject: headers.find(h => h.name === 'Subject')?.value || '', 694 | from: headers.find(h => h.name === 'From')?.value || '', 695 | date: headers.find(h => h.name === 'Date')?.value || '', 696 | }; 697 | }) 698 | ); 699 | 700 | return { 701 | content: [ 702 | { 703 | type: "text", 704 | text: results.map(r => 705 | `ID: ${r.id}\nSubject: ${r.subject}\nFrom: ${r.from}\nDate: ${r.date}\n` 706 | ).join('\n'), 707 | }, 708 | ], 709 | }; 710 | } 711 | 712 | // Updated implementation for the modify_email handler 713 | case "modify_email": { 714 | const validatedArgs = ModifyEmailSchema.parse(args); 715 | 716 | // Prepare request body 717 | const requestBody: any = {}; 718 | 719 | if (validatedArgs.labelIds) { 720 | requestBody.addLabelIds = validatedArgs.labelIds; 721 | } 722 | 723 | if (validatedArgs.addLabelIds) { 724 | requestBody.addLabelIds = validatedArgs.addLabelIds; 725 | } 726 | 727 | if (validatedArgs.removeLabelIds) { 728 | requestBody.removeLabelIds = validatedArgs.removeLabelIds; 729 | } 730 | 731 | await gmail.users.messages.modify({ 732 | userId: 'me', 733 | id: validatedArgs.messageId, 734 | requestBody: requestBody, 735 | }); 736 | 737 | return { 738 | content: [ 739 | { 740 | type: "text", 741 | text: `Email ${validatedArgs.messageId} labels updated successfully`, 742 | }, 743 | ], 744 | }; 745 | } 746 | 747 | case "delete_email": { 748 | const validatedArgs = DeleteEmailSchema.parse(args); 749 | await gmail.users.messages.delete({ 750 | userId: 'me', 751 | id: validatedArgs.messageId, 752 | }); 753 | 754 | return { 755 | content: [ 756 | { 757 | type: "text", 758 | text: `Email ${validatedArgs.messageId} deleted successfully`, 759 | }, 760 | ], 761 | }; 762 | } 763 | 764 | case "list_email_labels": { 765 | const labelResults = await listLabels(gmail); 766 | const systemLabels = labelResults.system; 767 | const userLabels = labelResults.user; 768 | 769 | return { 770 | content: [ 771 | { 772 | type: "text", 773 | text: `Found ${labelResults.count.total} labels (${labelResults.count.system} system, ${labelResults.count.user} user):\n\n` + 774 | "System Labels:\n" + 775 | systemLabels.map((l: GmailLabel) => `ID: ${l.id}\nName: ${l.name}\n`).join('\n') + 776 | "\nUser Labels:\n" + 777 | userLabels.map((l: GmailLabel) => `ID: ${l.id}\nName: ${l.name}\n`).join('\n') 778 | }, 779 | ], 780 | }; 781 | } 782 | 783 | case "batch_modify_emails": { 784 | const validatedArgs = BatchModifyEmailsSchema.parse(args); 785 | const messageIds = validatedArgs.messageIds; 786 | const batchSize = validatedArgs.batchSize || 50; 787 | 788 | // Prepare request body 789 | const requestBody: any = {}; 790 | 791 | if (validatedArgs.addLabelIds) { 792 | requestBody.addLabelIds = validatedArgs.addLabelIds; 793 | } 794 | 795 | if (validatedArgs.removeLabelIds) { 796 | requestBody.removeLabelIds = validatedArgs.removeLabelIds; 797 | } 798 | 799 | // Process messages in batches 800 | const { successes, failures } = await processBatches( 801 | messageIds, 802 | batchSize, 803 | async (batch) => { 804 | const results = await Promise.all( 805 | batch.map(async (messageId) => { 806 | const result = await gmail.users.messages.modify({ 807 | userId: 'me', 808 | id: messageId, 809 | requestBody: requestBody, 810 | }); 811 | return { messageId, success: true }; 812 | }) 813 | ); 814 | return results; 815 | } 816 | ); 817 | 818 | // Generate summary of the operation 819 | const successCount = successes.length; 820 | const failureCount = failures.length; 821 | 822 | let resultText = `Batch label modification complete.\n`; 823 | resultText += `Successfully processed: ${successCount} messages\n`; 824 | 825 | if (failureCount > 0) { 826 | resultText += `Failed to process: ${failureCount} messages\n\n`; 827 | resultText += `Failed message IDs:\n`; 828 | resultText += failures.map(f => `- ${(f.item as string).substring(0, 16)}... (${f.error.message})`).join('\n'); 829 | } 830 | 831 | return { 832 | content: [ 833 | { 834 | type: "text", 835 | text: resultText, 836 | }, 837 | ], 838 | }; 839 | } 840 | 841 | case "batch_delete_emails": { 842 | const validatedArgs = BatchDeleteEmailsSchema.parse(args); 843 | const messageIds = validatedArgs.messageIds; 844 | const batchSize = validatedArgs.batchSize || 50; 845 | 846 | // Process messages in batches 847 | const { successes, failures } = await processBatches( 848 | messageIds, 849 | batchSize, 850 | async (batch) => { 851 | const results = await Promise.all( 852 | batch.map(async (messageId) => { 853 | await gmail.users.messages.delete({ 854 | userId: 'me', 855 | id: messageId, 856 | }); 857 | return { messageId, success: true }; 858 | }) 859 | ); 860 | return results; 861 | } 862 | ); 863 | 864 | // Generate summary of the operation 865 | const successCount = successes.length; 866 | const failureCount = failures.length; 867 | 868 | let resultText = `Batch delete operation complete.\n`; 869 | resultText += `Successfully deleted: ${successCount} messages\n`; 870 | 871 | if (failureCount > 0) { 872 | resultText += `Failed to delete: ${failureCount} messages\n\n`; 873 | resultText += `Failed message IDs:\n`; 874 | resultText += failures.map(f => `- ${(f.item as string).substring(0, 16)}... (${f.error.message})`).join('\n'); 875 | } 876 | 877 | return { 878 | content: [ 879 | { 880 | type: "text", 881 | text: resultText, 882 | }, 883 | ], 884 | }; 885 | } 886 | 887 | // New label management handlers 888 | case "create_label": { 889 | const validatedArgs = CreateLabelSchema.parse(args); 890 | const result = await createLabel(gmail, validatedArgs.name, { 891 | messageListVisibility: validatedArgs.messageListVisibility, 892 | labelListVisibility: validatedArgs.labelListVisibility, 893 | }); 894 | 895 | return { 896 | content: [ 897 | { 898 | type: "text", 899 | text: `Label created successfully:\nID: ${result.id}\nName: ${result.name}\nType: ${result.type}`, 900 | }, 901 | ], 902 | }; 903 | } 904 | 905 | case "update_label": { 906 | const validatedArgs = UpdateLabelSchema.parse(args); 907 | 908 | // Prepare request body with only the fields that were provided 909 | const updates: any = {}; 910 | if (validatedArgs.name) updates.name = validatedArgs.name; 911 | if (validatedArgs.messageListVisibility) updates.messageListVisibility = validatedArgs.messageListVisibility; 912 | if (validatedArgs.labelListVisibility) updates.labelListVisibility = validatedArgs.labelListVisibility; 913 | 914 | const result = await updateLabel(gmail, validatedArgs.id, updates); 915 | 916 | return { 917 | content: [ 918 | { 919 | type: "text", 920 | text: `Label updated successfully:\nID: ${result.id}\nName: ${result.name}\nType: ${result.type}`, 921 | }, 922 | ], 923 | }; 924 | } 925 | 926 | case "delete_label": { 927 | const validatedArgs = DeleteLabelSchema.parse(args); 928 | const result = await deleteLabel(gmail, validatedArgs.id); 929 | 930 | return { 931 | content: [ 932 | { 933 | type: "text", 934 | text: result.message, 935 | }, 936 | ], 937 | }; 938 | } 939 | 940 | case "get_or_create_label": { 941 | const validatedArgs = GetOrCreateLabelSchema.parse(args); 942 | const result = await getOrCreateLabel(gmail, validatedArgs.name, { 943 | messageListVisibility: validatedArgs.messageListVisibility, 944 | labelListVisibility: validatedArgs.labelListVisibility, 945 | }); 946 | 947 | const action = result.type === 'user' && result.name === validatedArgs.name ? 'found existing' : 'created new'; 948 | 949 | return { 950 | content: [ 951 | { 952 | type: "text", 953 | text: `Successfully ${action} label:\nID: ${result.id}\nName: ${result.name}\nType: ${result.type}`, 954 | }, 955 | ], 956 | }; 957 | } 958 | 959 | 960 | // Filter management handlers 961 | case "create_filter": { 962 | const validatedArgs = CreateFilterSchema.parse(args); 963 | const result = await createFilter(gmail, validatedArgs.criteria, validatedArgs.action); 964 | 965 | // Format criteria for display 966 | const criteriaText = Object.entries(validatedArgs.criteria) 967 | .filter(([_, value]) => value !== undefined) 968 | .map(([key, value]) => `${key}: ${value}`) 969 | .join(', '); 970 | 971 | // Format actions for display 972 | const actionText = Object.entries(validatedArgs.action) 973 | .filter(([_, value]) => value !== undefined && (Array.isArray(value) ? value.length > 0 : true)) 974 | .map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(', ') : value}`) 975 | .join(', '); 976 | 977 | return { 978 | content: [ 979 | { 980 | type: "text", 981 | text: `Filter created successfully:\nID: ${result.id}\nCriteria: ${criteriaText}\nActions: ${actionText}`, 982 | }, 983 | ], 984 | }; 985 | } 986 | 987 | case "list_filters": { 988 | const result = await listFilters(gmail); 989 | const filters = result.filters; 990 | 991 | if (filters.length === 0) { 992 | return { 993 | content: [ 994 | { 995 | type: "text", 996 | text: "No filters found.", 997 | }, 998 | ], 999 | }; 1000 | } 1001 | 1002 | const filtersText = filters.map((filter: any) => { 1003 | const criteriaEntries = Object.entries(filter.criteria || {}) 1004 | .filter(([_, value]) => value !== undefined) 1005 | .map(([key, value]) => `${key}: ${value}`) 1006 | .join(', '); 1007 | 1008 | const actionEntries = Object.entries(filter.action || {}) 1009 | .filter(([_, value]) => value !== undefined && (Array.isArray(value) ? value.length > 0 : true)) 1010 | .map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(', ') : value}`) 1011 | .join(', '); 1012 | 1013 | return `ID: ${filter.id}\nCriteria: ${criteriaEntries}\nActions: ${actionEntries}\n`; 1014 | }).join('\n'); 1015 | 1016 | return { 1017 | content: [ 1018 | { 1019 | type: "text", 1020 | text: `Found ${result.count} filters:\n\n${filtersText}`, 1021 | }, 1022 | ], 1023 | }; 1024 | } 1025 | 1026 | case "get_filter": { 1027 | const validatedArgs = GetFilterSchema.parse(args); 1028 | const result = await getFilter(gmail, validatedArgs.filterId); 1029 | 1030 | const criteriaText = Object.entries(result.criteria || {}) 1031 | .filter(([_, value]) => value !== undefined) 1032 | .map(([key, value]) => `${key}: ${value}`) 1033 | .join(', '); 1034 | 1035 | const actionText = Object.entries(result.action || {}) 1036 | .filter(([_, value]) => value !== undefined && (Array.isArray(value) ? value.length > 0 : true)) 1037 | .map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(', ') : value}`) 1038 | .join(', '); 1039 | 1040 | return { 1041 | content: [ 1042 | { 1043 | type: "text", 1044 | text: `Filter details:\nID: ${result.id}\nCriteria: ${criteriaText}\nActions: ${actionText}`, 1045 | }, 1046 | ], 1047 | }; 1048 | } 1049 | 1050 | case "delete_filter": { 1051 | const validatedArgs = DeleteFilterSchema.parse(args); 1052 | const result = await deleteFilter(gmail, validatedArgs.filterId); 1053 | 1054 | return { 1055 | content: [ 1056 | { 1057 | type: "text", 1058 | text: result.message, 1059 | }, 1060 | ], 1061 | }; 1062 | } 1063 | 1064 | case "create_filter_from_template": { 1065 | const validatedArgs = CreateFilterFromTemplateSchema.parse(args); 1066 | const template = validatedArgs.template; 1067 | const params = validatedArgs.parameters; 1068 | 1069 | let filterConfig; 1070 | 1071 | switch (template) { 1072 | case 'fromSender': 1073 | if (!params.senderEmail) throw new Error("senderEmail is required for fromSender template"); 1074 | filterConfig = filterTemplates.fromSender(params.senderEmail, params.labelIds, params.archive); 1075 | break; 1076 | case 'withSubject': 1077 | if (!params.subjectText) throw new Error("subjectText is required for withSubject template"); 1078 | filterConfig = filterTemplates.withSubject(params.subjectText, params.labelIds, params.markAsRead); 1079 | break; 1080 | case 'withAttachments': 1081 | filterConfig = filterTemplates.withAttachments(params.labelIds); 1082 | break; 1083 | case 'largeEmails': 1084 | if (!params.sizeInBytes) throw new Error("sizeInBytes is required for largeEmails template"); 1085 | filterConfig = filterTemplates.largeEmails(params.sizeInBytes, params.labelIds); 1086 | break; 1087 | case 'containingText': 1088 | if (!params.searchText) throw new Error("searchText is required for containingText template"); 1089 | filterConfig = filterTemplates.containingText(params.searchText, params.labelIds, params.markImportant); 1090 | break; 1091 | case 'mailingList': 1092 | if (!params.listIdentifier) throw new Error("listIdentifier is required for mailingList template"); 1093 | filterConfig = filterTemplates.mailingList(params.listIdentifier, params.labelIds, params.archive); 1094 | break; 1095 | default: 1096 | throw new Error(`Unknown template: ${template}`); 1097 | } 1098 | 1099 | const result = await createFilter(gmail, filterConfig.criteria, filterConfig.action); 1100 | 1101 | return { 1102 | content: [ 1103 | { 1104 | type: "text", 1105 | text: `Filter created from template '${template}':\nID: ${result.id}\nTemplate used: ${template}`, 1106 | }, 1107 | ], 1108 | }; 1109 | } 1110 | case "download_attachment": { 1111 | const validatedArgs = DownloadAttachmentSchema.parse(args); 1112 | 1113 | try { 1114 | // Get the attachment data from Gmail API 1115 | const attachmentResponse = await gmail.users.messages.attachments.get({ 1116 | userId: 'me', 1117 | messageId: validatedArgs.messageId, 1118 | id: validatedArgs.attachmentId, 1119 | }); 1120 | 1121 | if (!attachmentResponse.data.data) { 1122 | throw new Error('No attachment data received'); 1123 | } 1124 | 1125 | // Decode the base64 data 1126 | const data = attachmentResponse.data.data; 1127 | const buffer = Buffer.from(data, 'base64url'); 1128 | 1129 | // Determine save path and filename 1130 | const savePath = validatedArgs.savePath || process.cwd(); 1131 | let filename = validatedArgs.filename; 1132 | 1133 | if (!filename) { 1134 | // Get original filename from message if not provided 1135 | const messageResponse = await gmail.users.messages.get({ 1136 | userId: 'me', 1137 | id: validatedArgs.messageId, 1138 | format: 'full', 1139 | }); 1140 | 1141 | // Find the attachment part to get original filename 1142 | const findAttachment = (part: any): string | null => { 1143 | if (part.body && part.body.attachmentId === validatedArgs.attachmentId) { 1144 | return part.filename || `attachment-${validatedArgs.attachmentId}`; 1145 | } 1146 | if (part.parts) { 1147 | for (const subpart of part.parts) { 1148 | const found = findAttachment(subpart); 1149 | if (found) return found; 1150 | } 1151 | } 1152 | return null; 1153 | }; 1154 | 1155 | filename = findAttachment(messageResponse.data.payload) || `attachment-${validatedArgs.attachmentId}`; 1156 | } 1157 | 1158 | // Ensure save directory exists 1159 | if (!fs.existsSync(savePath)) { 1160 | fs.mkdirSync(savePath, { recursive: true }); 1161 | } 1162 | 1163 | // Write file 1164 | const fullPath = path.join(savePath, filename); 1165 | fs.writeFileSync(fullPath, buffer); 1166 | 1167 | return { 1168 | content: [ 1169 | { 1170 | type: "text", 1171 | text: `Attachment downloaded successfully:\nFile: ${filename}\nSize: ${buffer.length} bytes\nSaved to: ${fullPath}`, 1172 | }, 1173 | ], 1174 | }; 1175 | } catch (error: any) { 1176 | return { 1177 | content: [ 1178 | { 1179 | type: "text", 1180 | text: `Failed to download attachment: ${error.message}`, 1181 | }, 1182 | ], 1183 | }; 1184 | } 1185 | } 1186 | 1187 | default: 1188 | throw new Error(`Unknown tool: ${name}`); 1189 | } 1190 | } catch (error: any) { 1191 | return { 1192 | content: [ 1193 | { 1194 | type: "text", 1195 | text: `Error: ${error.message}`, 1196 | }, 1197 | ], 1198 | }; 1199 | } 1200 | }); 1201 | 1202 | const transport = new StdioServerTransport(); 1203 | server.connect(transport); 1204 | } 1205 | 1206 | main().catch((error) => { 1207 | console.error('Server error:', error); 1208 | process.exit(1); 1209 | }); 1210 | --------------------------------------------------------------------------------