gmail-workspace-mcp-server 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Gmail Workspace MCP Server
2
2
 
3
- An MCP (Model Context Protocol) server that provides Gmail integration for AI assistants using Google Workspace service accounts with domain-wide delegation.
3
+ An MCP (Model Context Protocol) server that provides Gmail integration for AI assistants. Supports both **personal Gmail accounts** (via OAuth2) and **Google Workspace accounts** (via service account with domain-wide delegation).
4
4
 
5
5
  ## Features
6
6
 
@@ -10,7 +10,7 @@ An MCP (Model Context Protocol) server that provides Gmail integration for AI as
10
10
  - **Manage Emails**: Mark as read/unread, star/unstar, archive, apply labels
11
11
  - **Create Drafts**: Compose draft emails with reply support
12
12
  - **Send Emails**: Send new emails or replies, directly or from drafts
13
- - **Service Account Authentication**: Secure domain-wide delegation for Google Workspace organizations
13
+ - **Two Auth Methods**: OAuth2 for personal accounts, Service Account for Google Workspace
14
14
 
15
15
  ## Installation
16
16
 
@@ -24,11 +24,86 @@ Or run directly with npx:
24
24
  npx gmail-workspace-mcp-server
25
25
  ```
26
26
 
27
- ## Prerequisites
27
+ ## Authentication
28
28
 
29
- This server requires a Google Cloud service account with domain-wide delegation to access Gmail on behalf of users in your Google Workspace domain.
29
+ This server supports two authentication modes. Choose the one that matches your account type.
30
30
 
31
- ### Setup Steps
31
+ ### Option 1: OAuth2 (Personal Gmail Accounts)
32
+
33
+ Use this for personal `@gmail.com` accounts or any Google account without Workspace admin access.
34
+
35
+ #### Prerequisites
36
+
37
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
38
+ 2. Create or select a project
39
+ 3. Enable the Gmail API
40
+ 4. Configure the OAuth consent screen:
41
+ - Choose **External** user type
42
+ - Add yourself as a test user
43
+ - **Publish the app** (click "Publish" on the consent screen) to prevent refresh tokens from expiring every 7 days. No full Google verification is needed for personal use.
44
+ 5. Create OAuth 2.0 credentials:
45
+ - Choose **Desktop app** as the application type
46
+ - **Important**: Create new credentials _after_ publishing the app
47
+ 6. Copy the Client ID and Client Secret
48
+
49
+ #### Getting a Refresh Token
50
+
51
+ Run the one-time setup script from a clone of the repository:
52
+
53
+ ```bash
54
+ git clone https://github.com/pulsemcp/mcp-servers.git
55
+ cd mcp-servers/experimental/gmail
56
+ npx tsx scripts/oauth-setup.ts <client_id> <client_secret>
57
+ ```
58
+
59
+ You can also pass credentials via environment variables:
60
+
61
+ ```bash
62
+ GMAIL_OAUTH_CLIENT_ID=... GMAIL_OAUTH_CLIENT_SECRET=... npx tsx scripts/oauth-setup.ts
63
+ ```
64
+
65
+ **Port conflict?** If port 3000 is already in use, specify a different port:
66
+
67
+ ```bash
68
+ PORT=3001 npx tsx scripts/oauth-setup.ts <client_id> <client_secret>
69
+ ```
70
+
71
+ Desktop app credentials automatically allow `http://localhost` redirects on any port, so no additional Google Cloud Console configuration is needed.
72
+
73
+ This will open your browser for Google sign-in and print the refresh token. You only need to do this once — the refresh token does not expire (as long as the OAuth consent screen is published).
74
+
75
+ #### Environment Variables (OAuth2)
76
+
77
+ | Variable | Required | Description |
78
+ | --------------------------- | -------- | -------------------------------------------------- |
79
+ | `GMAIL_OAUTH_CLIENT_ID` | Yes | OAuth2 client ID from Google Cloud Console |
80
+ | `GMAIL_OAUTH_CLIENT_SECRET` | Yes | OAuth2 client secret |
81
+ | `GMAIL_OAUTH_REFRESH_TOKEN` | Yes | Refresh token from the setup script |
82
+ | `GMAIL_ENABLED_TOOLGROUPS` | No | Comma-separated list of tool groups (default: all) |
83
+
84
+ #### Claude Desktop Configuration (OAuth2)
85
+
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "gmail": {
90
+ "command": "npx",
91
+ "args": ["gmail-workspace-mcp-server"],
92
+ "env": {
93
+ "GMAIL_OAUTH_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
94
+ "GMAIL_OAUTH_CLIENT_SECRET": "your-client-secret",
95
+ "GMAIL_OAUTH_REFRESH_TOKEN": "your-refresh-token"
96
+ }
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### Option 2: Service Account (Google Workspace)
103
+
104
+ Use this for Google Workspace organizations where a domain admin can grant domain-wide delegation.
105
+
106
+ #### Prerequisites
32
107
 
33
108
  1. Go to [Google Cloud Console](https://console.cloud.google.com/)
34
109
  2. Create or select a project
@@ -41,7 +116,7 @@ This server requires a Google Cloud service account with domain-wide delegation
41
116
  - `https://www.googleapis.com/auth/gmail.send` (send emails)
42
117
  6. Download the JSON key file
43
118
 
44
- ### Environment Variables
119
+ #### Environment Variables (Service Account)
45
120
 
46
121
  | Variable | Required | Description |
47
122
  | ------------------------------------ | -------- | ------------------------------------------------------------------- |
@@ -52,7 +127,30 @@ This server requires a Google Cloud service account with domain-wide delegation
52
127
 
53
128
  You can find the `client_email` and `private_key` values in your service account JSON key file.
54
129
 
55
- ### Tool Groups
130
+ #### Claude Desktop Configuration (Service Account)
131
+
132
+ ```json
133
+ {
134
+ "mcpServers": {
135
+ "gmail": {
136
+ "command": "npx",
137
+ "args": ["gmail-workspace-mcp-server"],
138
+ "env": {
139
+ "GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL": "my-service-account@my-project.iam.gserviceaccount.com",
140
+ "GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----",
141
+ "GMAIL_IMPERSONATE_EMAIL": "user@yourdomain.com"
142
+ }
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ **Note:** For the private key, you can either:
149
+
150
+ 1. Use the key directly with `\n` for newlines (as shown above)
151
+ 2. Set the environment variable from a shell that preserves newlines
152
+
153
+ ## Tool Groups
56
154
 
57
155
  The server supports three tool groups for permission-based access control:
58
156
 
@@ -77,33 +175,6 @@ GMAIL_ENABLED_TOOLGROUPS=readwrite_external
77
175
 
78
176
  **Security Note:** The `send_email` tool is in a separate `readwrite_external` group because it can send emails externally, which carries higher risk than internal operations like modifying labels or creating drafts.
79
177
 
80
- ## Configuration
81
-
82
- ### Claude Desktop
83
-
84
- Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json`):
85
-
86
- ```json
87
- {
88
- "mcpServers": {
89
- "gmail": {
90
- "command": "npx",
91
- "args": ["gmail-workspace-mcp-server"],
92
- "env": {
93
- "GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL": "my-service-account@my-project.iam.gserviceaccount.com",
94
- "GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----",
95
- "GMAIL_IMPERSONATE_EMAIL": "user@yourdomain.com"
96
- }
97
- }
98
- }
99
- }
100
- ```
101
-
102
- **Note:** For the private key, you can either:
103
-
104
- 1. Use the key directly with `\n` for newlines (as shown above)
105
- 2. Set the environment variable from a shell that preserves newlines
106
-
107
178
  ## Available Tools
108
179
 
109
180
  ### list_email_conversations
@@ -262,7 +333,7 @@ npm test
262
333
  # Run integration tests
263
334
  npm run test:integration
264
335
 
265
- # Run manual tests (requires service account credentials)
336
+ # Run manual tests (requires credentials)
266
337
  npm run test:manual
267
338
  ```
268
339
 
package/build/index.js CHANGED
@@ -14,40 +14,95 @@ const VERSION = packageJson.version;
14
14
  // ENVIRONMENT VALIDATION
15
15
  // =============================================================================
16
16
  function validateEnvironment() {
17
- const missing = [];
18
- if (!process.env.GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL) {
19
- missing.push('GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL');
17
+ // Check for OAuth2 credentials
18
+ const hasOAuth2 = process.env.GMAIL_OAUTH_CLIENT_ID &&
19
+ process.env.GMAIL_OAUTH_CLIENT_SECRET &&
20
+ process.env.GMAIL_OAUTH_REFRESH_TOKEN;
21
+ // Check for service account credentials
22
+ const hasServiceAccount = process.env.GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL &&
23
+ process.env.GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY &&
24
+ process.env.GMAIL_IMPERSONATE_EMAIL;
25
+ if (hasOAuth2 || hasServiceAccount) {
26
+ return;
20
27
  }
21
- if (!process.env.GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY) {
22
- missing.push('GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY');
23
- }
24
- if (!process.env.GMAIL_IMPERSONATE_EMAIL) {
25
- missing.push('GMAIL_IMPERSONATE_EMAIL');
28
+ // Check for partial OAuth2 configuration
29
+ const oauthVars = {
30
+ GMAIL_OAUTH_CLIENT_ID: process.env.GMAIL_OAUTH_CLIENT_ID,
31
+ GMAIL_OAUTH_CLIENT_SECRET: process.env.GMAIL_OAUTH_CLIENT_SECRET,
32
+ GMAIL_OAUTH_REFRESH_TOKEN: process.env.GMAIL_OAUTH_REFRESH_TOKEN,
33
+ };
34
+ const hasPartialOAuth2 = Object.values(oauthVars).some(Boolean);
35
+ if (hasPartialOAuth2) {
36
+ const missingOAuth = Object.entries(oauthVars)
37
+ .filter(([, v]) => !v)
38
+ .map(([k]) => k);
39
+ logError('validateEnvironment', 'Incomplete OAuth2 configuration. Missing:');
40
+ for (const varName of missingOAuth) {
41
+ console.error(` - ${varName}`);
42
+ }
43
+ console.error('\nOAuth2 mode requires all three variables:');
44
+ console.error(' GMAIL_OAUTH_CLIENT_ID: OAuth2 client ID from Google Cloud Console');
45
+ console.error(' GMAIL_OAUTH_CLIENT_SECRET: OAuth2 client secret');
46
+ console.error(' GMAIL_OAUTH_REFRESH_TOKEN: Refresh token from one-time consent flow');
47
+ console.error('\nRun the setup script to obtain a refresh token:');
48
+ console.error(' npx tsx scripts/oauth-setup.ts <client_id> <client_secret>');
49
+ console.error('\n======================================================\n');
50
+ process.exit(1);
26
51
  }
27
- if (missing.length > 0) {
28
- logError('validateEnvironment', 'Missing required environment variables:');
29
- console.error('\nThis MCP server requires a Google Cloud service account with');
30
- console.error('domain-wide delegation to access Gmail on behalf of users.');
31
- console.error('\nRequired environment variables:');
52
+ // Check for partial service account configuration
53
+ const serviceAccountVars = {
54
+ GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL: process.env.GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL,
55
+ GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY: process.env.GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY,
56
+ GMAIL_IMPERSONATE_EMAIL: process.env.GMAIL_IMPERSONATE_EMAIL,
57
+ };
58
+ const hasPartialServiceAccount = Object.values(serviceAccountVars).some(Boolean);
59
+ if (hasPartialServiceAccount) {
60
+ const missingServiceAccount = Object.entries(serviceAccountVars)
61
+ .filter(([, v]) => !v)
62
+ .map(([k]) => k);
63
+ logError('validateEnvironment', 'Incomplete service account configuration. Missing:');
64
+ for (const varName of missingServiceAccount) {
65
+ console.error(` - ${varName}`);
66
+ }
67
+ console.error('\nService account mode requires all three variables:');
32
68
  console.error(' GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL: Service account email address');
33
- console.error(' Example: my-service-account@my-project.iam.gserviceaccount.com');
34
69
  console.error(' GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY: Service account private key (PEM format)');
35
- console.error(' Example: -----BEGIN PRIVATE KEY-----\\nMIIE...\\n-----END PRIVATE KEY-----');
36
70
  console.error(' GMAIL_IMPERSONATE_EMAIL: Email address to impersonate');
37
- console.error(' Example: user@yourdomain.com');
38
71
  console.error('\nSetup steps:');
39
72
  console.error(' 1. Go to https://console.cloud.google.com/');
40
73
  console.error(' 2. Create a service account with domain-wide delegation');
41
74
  console.error(' 3. In Google Workspace Admin, grant required Gmail API scopes');
42
75
  console.error(' 4. Download the JSON key file and extract client_email and private_key');
43
- console.error('\nOptional environment variables:');
44
- console.error(' GMAIL_ENABLED_TOOLGROUPS: Comma-separated list of tool groups to enable');
45
- console.error(' Valid groups: readonly, readwrite, readwrite_external');
46
- console.error(' Default: all groups enabled');
47
- console.error(' Example: GMAIL_ENABLED_TOOLGROUPS=readwrite');
48
76
  console.error('\n======================================================\n');
49
77
  process.exit(1);
50
78
  }
79
+ // No credentials found at all
80
+ logError('validateEnvironment', 'Missing required environment variables:');
81
+ console.error('\nThis MCP server supports two authentication modes:\n');
82
+ console.error('--- Option 1: OAuth2 (for personal Gmail accounts) ---');
83
+ console.error(' GMAIL_OAUTH_CLIENT_ID: OAuth2 client ID from Google Cloud Console');
84
+ console.error(' GMAIL_OAUTH_CLIENT_SECRET: OAuth2 client secret');
85
+ console.error(' GMAIL_OAUTH_REFRESH_TOKEN: Refresh token from one-time consent flow');
86
+ console.error('\n Setup: Run `npx tsx scripts/oauth-setup.ts <client_id> <client_secret>`');
87
+ console.error('\n--- Option 2: Service Account (for Google Workspace) ---');
88
+ console.error(' GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL: Service account email address');
89
+ console.error(' Example: my-service-account@my-project.iam.gserviceaccount.com');
90
+ console.error(' GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY: Service account private key (PEM format)');
91
+ console.error(' Example: -----BEGIN PRIVATE KEY-----\\nMIIE...\\n-----END PRIVATE KEY-----');
92
+ console.error(' GMAIL_IMPERSONATE_EMAIL: Email address to impersonate');
93
+ console.error(' Example: user@yourdomain.com');
94
+ console.error('\n Setup steps:');
95
+ console.error(' 1. Go to https://console.cloud.google.com/');
96
+ console.error(' 2. Create a service account with domain-wide delegation');
97
+ console.error(' 3. In Google Workspace Admin, grant required Gmail API scopes');
98
+ console.error(' 4. Download the JSON key file and extract client_email and private_key');
99
+ console.error('\nOptional environment variables:');
100
+ console.error(' GMAIL_ENABLED_TOOLGROUPS: Comma-separated list of tool groups to enable');
101
+ console.error(' Valid groups: readonly, readwrite, readwrite_external');
102
+ console.error(' Default: all groups enabled');
103
+ console.error(' Example: GMAIL_ENABLED_TOOLGROUPS=readwrite');
104
+ console.error('\n======================================================\n');
105
+ process.exit(1);
51
106
  }
52
107
  // =============================================================================
53
108
  // MAIN ENTRY POINT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gmail-workspace-mcp-server",
3
- "version": "0.0.4",
4
- "description": "MCP server for Gmail integration with service account support",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Gmail integration with OAuth2 and service account support",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
7
7
  "bin": {
package/shared/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { createMCPServer, createDefaultClient, ServiceAccountGmailClient, type IGmailClient, type ClientFactory, type ServiceAccountCredentials, type CreateMCPServerOptions, type Draft, } from './server.js';
1
+ export { createMCPServer, createDefaultClient, ServiceAccountGmailClient, OAuth2GmailClient, GMAIL_SCOPES, type IGmailClient, type ClientFactory, type ServiceAccountCredentials, type CreateMCPServerOptions, type Draft, } from './server.js';
2
2
  export { createRegisterTools, registerTools, parseEnabledToolGroups, getAvailableToolGroups, type ToolGroup, } from './tools.js';
3
3
  export { logServerStart, logError, logWarning, logDebug } from './logging.js';
4
4
  export type { Email, EmailListItem, EmailHeader, EmailPart, Label, Thread, PaginatedResponse, } from './types.js';
package/shared/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Main exports for Gmail MCP Server
2
2
  // Server and client
3
- export { createMCPServer, createDefaultClient, ServiceAccountGmailClient, } from './server.js';
3
+ export { createMCPServer, createDefaultClient, ServiceAccountGmailClient, OAuth2GmailClient, GMAIL_SCOPES, } from './server.js';
4
4
  // Tools and tool groups
5
5
  export { createRegisterTools, registerTools, parseEnabledToolGroups, getAvailableToolGroups, } from './tools.js';
6
6
  // Logging utilities
@@ -1,5 +1,10 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import type { Email, EmailListItem } from './types.js';
3
+ /**
4
+ * Gmail API scopes required by this server.
5
+ * Shared between service account JWT, OAuth2 consent flow, and documentation.
6
+ */
7
+ export declare const GMAIL_SCOPES: readonly ["https://www.googleapis.com/auth/gmail.readonly", "https://www.googleapis.com/auth/gmail.modify", "https://www.googleapis.com/auth/gmail.compose", "https://www.googleapis.com/auth/gmail.send"];
3
8
  /**
4
9
  * Draft message structure
5
10
  */
@@ -108,18 +113,32 @@ export interface ServiceAccountCredentials {
108
113
  client_x509_cert_url: string;
109
114
  }
110
115
  /**
111
- * Gmail API client implementation using service account with domain-wide delegation
116
+ * Abstract base class for Gmail API clients.
117
+ * Provides shared token management (caching, mutex refresh) and all
118
+ * IGmailClient method implementations. Subclasses only need to implement
119
+ * the authentication-specific methods.
112
120
  */
113
- export declare class ServiceAccountGmailClient implements IGmailClient {
114
- private impersonateEmail;
115
- private baseUrl;
116
- private jwtClient;
121
+ declare abstract class BaseGmailClient implements IGmailClient {
122
+ protected baseUrl: string;
117
123
  private cachedToken;
118
124
  private tokenExpiry;
119
125
  private refreshPromise;
120
- constructor(credentials: ServiceAccountCredentials, impersonateEmail: string);
126
+ /**
127
+ * Perform the authentication-specific token refresh.
128
+ * Must set this.cachedToken and this.tokenExpiry via updateToken().
129
+ */
130
+ protected abstract refreshTokenImpl(): Promise<{
131
+ token: string;
132
+ expiryDate: number;
133
+ }>;
134
+ /**
135
+ * Get the sender email address for composing/sending emails.
136
+ * Service account uses the impersonation email; OAuth2 fetches from profile API.
137
+ */
138
+ protected abstract getSenderEmail(): Promise<string>;
139
+ protected updateToken(token: string, expiryDate: number): void;
121
140
  private refreshToken;
122
- private getHeaders;
141
+ protected getHeaders(): Promise<Record<string, string>>;
123
142
  listMessages(options?: {
124
143
  q?: string;
125
144
  maxResults?: number;
@@ -173,16 +192,58 @@ export declare class ServiceAccountGmailClient implements IGmailClient {
173
192
  }): Promise<Email>;
174
193
  sendDraft(draftId: string): Promise<Email>;
175
194
  }
195
+ /**
196
+ * Gmail API client implementation using service account with domain-wide delegation
197
+ */
198
+ export declare class ServiceAccountGmailClient extends BaseGmailClient {
199
+ private jwtClient;
200
+ private impersonateEmail;
201
+ constructor(credentials: ServiceAccountCredentials, impersonateEmail: string);
202
+ protected refreshTokenImpl(): Promise<{
203
+ token: string;
204
+ expiryDate: number;
205
+ }>;
206
+ protected getSenderEmail(): Promise<string>;
207
+ }
208
+ /**
209
+ * Gmail API client implementation using OAuth2 user authentication.
210
+ * Enables access to personal Gmail accounts (e.g., @gmail.com) that cannot
211
+ * use domain-wide delegation.
212
+ */
213
+ export declare class OAuth2GmailClient extends BaseGmailClient {
214
+ private oauth2Client;
215
+ private userEmail;
216
+ constructor(clientId: string, clientSecret: string, refreshToken: string);
217
+ protected refreshTokenImpl(): Promise<{
218
+ token: string;
219
+ expiryDate: number;
220
+ }>;
221
+ /**
222
+ * Fetches the authenticated user's email address from the Gmail profile API.
223
+ * Caches the result for subsequent calls.
224
+ */
225
+ protected getSenderEmail(): Promise<string>;
226
+ }
176
227
  export type ClientFactory = () => IGmailClient;
177
228
  export interface CreateMCPServerOptions {
178
229
  version: string;
179
230
  }
180
231
  /**
181
232
  * Creates the default Gmail client based on environment variables.
182
- * Uses service account with domain-wide delegation:
233
+ *
234
+ * Supports two authentication modes:
235
+ *
236
+ * 1. OAuth2 (for personal Gmail accounts):
237
+ * - GMAIL_OAUTH_CLIENT_ID: OAuth2 client ID from Google Cloud Console
238
+ * - GMAIL_OAUTH_CLIENT_SECRET: OAuth2 client secret
239
+ * - GMAIL_OAUTH_REFRESH_TOKEN: Refresh token from one-time consent flow
240
+ *
241
+ * 2. Service Account (for Google Workspace accounts):
183
242
  * - GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL: Service account email address
184
243
  * - GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY: Service account private key (PEM format)
185
244
  * - GMAIL_IMPERSONATE_EMAIL: Email address to impersonate
245
+ *
246
+ * If OAuth2 credentials are present, OAuth2 mode is used. Otherwise, service account mode is used.
186
247
  */
187
248
  export declare function createDefaultClient(): IGmailClient;
188
249
  export declare function createMCPServer(options: CreateMCPServerOptions): {
@@ -222,3 +283,4 @@ export declare function createMCPServer(options: CreateMCPServerOptions): {
222
283
  }>;
223
284
  registerHandlers: (server: Server, clientFactory?: ClientFactory) => Promise<void>;
224
285
  };
286
+ export {};
package/shared/server.js CHANGED
@@ -1,38 +1,34 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { JWT } from 'google-auth-library';
2
+ import { JWT, OAuth2Client } from 'google-auth-library';
3
3
  import { createRegisterTools } from './tools.js';
4
4
  /**
5
- * Gmail API client implementation using service account with domain-wide delegation
5
+ * Gmail API scopes required by this server.
6
+ * Shared between service account JWT, OAuth2 consent flow, and documentation.
6
7
  */
7
- export class ServiceAccountGmailClient {
8
- impersonateEmail;
8
+ export const GMAIL_SCOPES = [
9
+ 'https://www.googleapis.com/auth/gmail.readonly',
10
+ 'https://www.googleapis.com/auth/gmail.modify',
11
+ 'https://www.googleapis.com/auth/gmail.compose',
12
+ 'https://www.googleapis.com/auth/gmail.send',
13
+ ];
14
+ /**
15
+ * Abstract base class for Gmail API clients.
16
+ * Provides shared token management (caching, mutex refresh) and all
17
+ * IGmailClient method implementations. Subclasses only need to implement
18
+ * the authentication-specific methods.
19
+ */
20
+ class BaseGmailClient {
9
21
  baseUrl = 'https://gmail.googleapis.com/gmail/v1/users/me';
10
- jwtClient;
11
22
  cachedToken = null;
12
23
  tokenExpiry = 0;
13
24
  refreshPromise = null;
14
- constructor(credentials, impersonateEmail) {
15
- this.impersonateEmail = impersonateEmail;
16
- this.jwtClient = new JWT({
17
- email: credentials.client_email,
18
- key: credentials.private_key,
19
- scopes: [
20
- 'https://www.googleapis.com/auth/gmail.readonly',
21
- 'https://www.googleapis.com/auth/gmail.modify',
22
- 'https://www.googleapis.com/auth/gmail.compose',
23
- 'https://www.googleapis.com/auth/gmail.send',
24
- ],
25
- subject: impersonateEmail,
26
- });
25
+ updateToken(token, expiryDate) {
26
+ this.cachedToken = token;
27
+ this.tokenExpiry = expiryDate;
27
28
  }
28
29
  async refreshToken() {
29
- const tokenResponse = await this.jwtClient.authorize();
30
- if (!tokenResponse.access_token) {
31
- throw new Error('Failed to obtain access token from service account');
32
- }
33
- this.cachedToken = tokenResponse.access_token;
34
- // Token typically expires in 1 hour, but use the actual expiry if provided
35
- this.tokenExpiry = tokenResponse.expiry_date || Date.now() + 3600000;
30
+ const { token, expiryDate } = await this.refreshTokenImpl();
31
+ this.updateToken(token, expiryDate);
36
32
  }
37
33
  async getHeaders() {
38
34
  // Check if we have a valid cached token (with 60 second buffer)
@@ -71,8 +67,9 @@ export class ServiceAccountGmailClient {
71
67
  }
72
68
  async createDraft(options) {
73
69
  const headers = await this.getHeaders();
70
+ const senderEmail = await this.getSenderEmail();
74
71
  const { createDraft } = await import('./gmail-client/lib/drafts.js');
75
- return createDraft(this.baseUrl, headers, this.impersonateEmail, options);
72
+ return createDraft(this.baseUrl, headers, senderEmail, options);
76
73
  }
77
74
  async getDraft(draftId) {
78
75
  const headers = await this.getHeaders();
@@ -91,8 +88,9 @@ export class ServiceAccountGmailClient {
91
88
  }
92
89
  async sendMessage(options) {
93
90
  const headers = await this.getHeaders();
91
+ const senderEmail = await this.getSenderEmail();
94
92
  const { sendMessage } = await import('./gmail-client/lib/send-message.js');
95
- return sendMessage(this.baseUrl, headers, this.impersonateEmail, options);
93
+ return sendMessage(this.baseUrl, headers, senderEmail, options);
96
94
  }
97
95
  async sendDraft(draftId) {
98
96
  const headers = await this.getHeaders();
@@ -100,14 +98,119 @@ export class ServiceAccountGmailClient {
100
98
  return sendDraft(this.baseUrl, headers, draftId);
101
99
  }
102
100
  }
101
+ /**
102
+ * Gmail API client implementation using service account with domain-wide delegation
103
+ */
104
+ export class ServiceAccountGmailClient extends BaseGmailClient {
105
+ jwtClient;
106
+ impersonateEmail;
107
+ constructor(credentials, impersonateEmail) {
108
+ super();
109
+ this.impersonateEmail = impersonateEmail;
110
+ this.jwtClient = new JWT({
111
+ email: credentials.client_email,
112
+ key: credentials.private_key,
113
+ scopes: [...GMAIL_SCOPES],
114
+ subject: impersonateEmail,
115
+ });
116
+ }
117
+ async refreshTokenImpl() {
118
+ const tokenResponse = await this.jwtClient.authorize();
119
+ if (!tokenResponse.access_token) {
120
+ throw new Error('Failed to obtain access token from service account');
121
+ }
122
+ return {
123
+ token: tokenResponse.access_token,
124
+ // Token typically expires in 1 hour, but use the actual expiry if provided
125
+ expiryDate: tokenResponse.expiry_date || Date.now() + 3600000,
126
+ };
127
+ }
128
+ async getSenderEmail() {
129
+ return this.impersonateEmail;
130
+ }
131
+ }
132
+ /**
133
+ * Gmail API client implementation using OAuth2 user authentication.
134
+ * Enables access to personal Gmail accounts (e.g., @gmail.com) that cannot
135
+ * use domain-wide delegation.
136
+ */
137
+ export class OAuth2GmailClient extends BaseGmailClient {
138
+ oauth2Client;
139
+ userEmail = null;
140
+ constructor(clientId, clientSecret, refreshToken) {
141
+ super();
142
+ this.oauth2Client = new OAuth2Client(clientId, clientSecret);
143
+ this.oauth2Client.setCredentials({ refresh_token: refreshToken });
144
+ }
145
+ async refreshTokenImpl() {
146
+ const { token, res } = await this.oauth2Client.getAccessToken();
147
+ if (!token) {
148
+ throw new Error('Failed to obtain access token from OAuth2 refresh token');
149
+ }
150
+ // Use expiry from response if available, otherwise default to 1 hour
151
+ const expiryDate = res?.data?.expiry_date;
152
+ return {
153
+ token,
154
+ expiryDate: typeof expiryDate === 'number' ? expiryDate : Date.now() + 3600000,
155
+ };
156
+ }
157
+ /**
158
+ * Fetches the authenticated user's email address from the Gmail profile API.
159
+ * Caches the result for subsequent calls.
160
+ */
161
+ async getSenderEmail() {
162
+ if (this.userEmail) {
163
+ return this.userEmail;
164
+ }
165
+ const headers = await this.getHeaders();
166
+ const response = await fetch(`${this.baseUrl}/profile`, {
167
+ method: 'GET',
168
+ headers,
169
+ });
170
+ if (!response.ok) {
171
+ const body = await response.text().catch(() => '');
172
+ throw new Error(`Failed to fetch Gmail profile: ${response.status} ${response.statusText}${body ? ` - ${body}` : ''}`);
173
+ }
174
+ const profile = (await response.json());
175
+ this.userEmail = profile.emailAddress;
176
+ return this.userEmail;
177
+ }
178
+ }
103
179
  /**
104
180
  * Creates the default Gmail client based on environment variables.
105
- * Uses service account with domain-wide delegation:
181
+ *
182
+ * Supports two authentication modes:
183
+ *
184
+ * 1. OAuth2 (for personal Gmail accounts):
185
+ * - GMAIL_OAUTH_CLIENT_ID: OAuth2 client ID from Google Cloud Console
186
+ * - GMAIL_OAUTH_CLIENT_SECRET: OAuth2 client secret
187
+ * - GMAIL_OAUTH_REFRESH_TOKEN: Refresh token from one-time consent flow
188
+ *
189
+ * 2. Service Account (for Google Workspace accounts):
106
190
  * - GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL: Service account email address
107
191
  * - GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY: Service account private key (PEM format)
108
192
  * - GMAIL_IMPERSONATE_EMAIL: Email address to impersonate
193
+ *
194
+ * If OAuth2 credentials are present, OAuth2 mode is used. Otherwise, service account mode is used.
109
195
  */
110
196
  export function createDefaultClient() {
197
+ // Check for OAuth2 credentials first
198
+ const oauthClientId = process.env.GMAIL_OAUTH_CLIENT_ID;
199
+ const oauthClientSecret = process.env.GMAIL_OAUTH_CLIENT_SECRET;
200
+ const oauthRefreshToken = process.env.GMAIL_OAUTH_REFRESH_TOKEN;
201
+ if (oauthClientId && oauthClientSecret && oauthRefreshToken) {
202
+ return new OAuth2GmailClient(oauthClientId, oauthClientSecret, oauthRefreshToken);
203
+ }
204
+ // Warn if some OAuth2 vars are set but not all (likely misconfiguration)
205
+ if (oauthClientId || oauthClientSecret || oauthRefreshToken) {
206
+ const missing = [
207
+ !oauthClientId && 'GMAIL_OAUTH_CLIENT_ID',
208
+ !oauthClientSecret && 'GMAIL_OAUTH_CLIENT_SECRET',
209
+ !oauthRefreshToken && 'GMAIL_OAUTH_REFRESH_TOKEN',
210
+ ].filter(Boolean);
211
+ console.warn(`Warning: Partial OAuth2 configuration detected. Missing: ${missing.join(', ')}. Falling back to service account mode.`);
212
+ }
213
+ // Fall back to service account mode
111
214
  const clientEmail = process.env.GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL;
112
215
  // Handle both literal \n in JSON configs and actual newlines
113
216
  const privateKey = process.env.GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY?.replace(/\\n/g, '\n');
@@ -3,10 +3,13 @@ import { z } from 'zod';
3
3
  import type { ClientFactory } from '../server.js';
4
4
  export declare const GetEmailConversationSchema: z.ZodObject<{
5
5
  email_id: z.ZodString;
6
+ include_html: z.ZodOptional<z.ZodBoolean>;
6
7
  }, "strip", z.ZodTypeAny, {
7
8
  email_id: string;
9
+ include_html?: boolean | undefined;
8
10
  }, {
9
11
  email_id: string;
12
+ include_html?: boolean | undefined;
10
13
  }>;
11
14
  export declare function getEmailConversationTool(server: Server, clientFactory: ClientFactory): {
12
15
  name: string;
@@ -18,6 +21,10 @@ export declare function getEmailConversationTool(server: Server, clientFactory:
18
21
  type: string;
19
22
  description: string;
20
23
  };
24
+ include_html: {
25
+ type: string;
26
+ description: string;
27
+ };
21
28
  };
22
29
  required: string[];
23
30
  };
@@ -3,9 +3,12 @@ import { getHeader } from '../utils/email-helpers.js';
3
3
  const PARAM_DESCRIPTIONS = {
4
4
  email_id: 'The unique identifier of the email to retrieve. ' +
5
5
  'Obtain this from list_email_conversations or search_email_conversations.',
6
+ include_html: 'When true, includes the raw HTML body of the email (if available) in addition to the plain text. ' +
7
+ 'Useful for rendering emails with original formatting, creating screenshots, or archival workflows.',
6
8
  };
7
9
  export const GetEmailConversationSchema = z.object({
8
10
  email_id: z.string().min(1).describe(PARAM_DESCRIPTIONS.email_id),
11
+ include_html: z.boolean().optional().describe(PARAM_DESCRIPTIONS.include_html),
9
12
  });
10
13
  const TOOL_DESCRIPTION = `Retrieve the full content of a specific email conversation by its ID.
11
14
 
@@ -13,11 +16,13 @@ Returns the complete email including headers, body content, and attachment infor
13
16
 
14
17
  **Parameters:**
15
18
  - email_id: The unique identifier of the email (required)
19
+ - include_html: When true, includes raw HTML body in addition to plain text (optional)
16
20
 
17
21
  **Returns:**
18
22
  Full email details including:
19
23
  - Subject, From, To, Cc, Date headers
20
24
  - Full message body (plain text preferred, HTML as fallback)
25
+ - Raw HTML body (when include_html is true and HTML content is available)
21
26
  - List of attachments (if any)
22
27
  - Labels assigned to the email
23
28
 
@@ -25,6 +30,8 @@ Full email details including:
25
30
  - Read the full content of an email after listing conversations
26
31
  - Extract specific information from an email body
27
32
  - Check attachment details
33
+ - Render emails with original HTML formatting (use include_html: true)
34
+ - Create email screenshots or archives with original styling
28
35
 
29
36
  **Note:** Use list_email_conversations or search_email_conversations first to get email IDs.`;
30
37
  /**
@@ -84,6 +91,22 @@ function getEmailBody(email) {
84
91
  }
85
92
  return '(No body content available)';
86
93
  }
94
+ /**
95
+ * Gets the raw HTML body content from an email (if available)
96
+ */
97
+ function getEmailHtmlBody(email) {
98
+ // Check if body is directly on payload and it's HTML
99
+ if (email.payload?.body?.data && email.payload.mimeType === 'text/html') {
100
+ return decodeBase64Url(email.payload.body.data);
101
+ }
102
+ // Try to extract HTML from parts
103
+ if (email.payload?.parts) {
104
+ const html = extractBodyContent(email.payload.parts, 'text/html');
105
+ if (html)
106
+ return html;
107
+ }
108
+ return null;
109
+ }
87
110
  /**
88
111
  * Extracts attachment information from email parts
89
112
  */
@@ -108,7 +131,7 @@ function getAttachments(parts) {
108
131
  /**
109
132
  * Formats an email for display
110
133
  */
111
- function formatFullEmail(email) {
134
+ function formatFullEmail(email, options) {
112
135
  const subject = getHeader(email, 'Subject') || '(No Subject)';
113
136
  const from = getHeader(email, 'From') || 'Unknown';
114
137
  const to = getHeader(email, 'To') || 'Unknown';
@@ -136,6 +159,26 @@ function formatFullEmail(email) {
136
159
  ## Body
137
160
 
138
161
  ${body}`;
162
+ // Include raw HTML body if requested
163
+ if (options?.includeHtml) {
164
+ const htmlBody = getEmailHtmlBody(email);
165
+ if (htmlBody) {
166
+ output += `
167
+
168
+ ## HTML Body
169
+
170
+ \`\`\`html
171
+ ${htmlBody}
172
+ \`\`\``;
173
+ }
174
+ else {
175
+ output += `
176
+
177
+ ## HTML Body
178
+
179
+ (No HTML content available)`;
180
+ }
181
+ }
139
182
  if (attachments.length > 0) {
140
183
  output += `\n\n## Attachments (${attachments.length})\n`;
141
184
  attachments.forEach((att, i) => {
@@ -156,6 +199,10 @@ export function getEmailConversationTool(server, clientFactory) {
156
199
  type: 'string',
157
200
  description: PARAM_DESCRIPTIONS.email_id,
158
201
  },
202
+ include_html: {
203
+ type: 'boolean',
204
+ description: PARAM_DESCRIPTIONS.include_html,
205
+ },
159
206
  },
160
207
  required: ['email_id'],
161
208
  },
@@ -166,7 +213,9 @@ export function getEmailConversationTool(server, clientFactory) {
166
213
  const email = await client.getMessage(parsed.email_id, {
167
214
  format: 'full',
168
215
  });
169
- const formattedEmail = formatFullEmail(email);
216
+ const formattedEmail = formatFullEmail(email, {
217
+ includeHtml: parsed.include_html,
218
+ });
170
219
  return {
171
220
  content: [
172
221
  {