gmail-workspace-mcp-server 0.1.1 → 0.2.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
@@ -48,24 +48,22 @@ Use this for personal `@gmail.com` accounts or any Google account without Worksp
48
48
 
49
49
  #### Getting a Refresh Token
50
50
 
51
- Run the one-time setup script from a clone of the repository:
51
+ Run the built-in setup command:
52
52
 
53
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>
54
+ npx gmail-workspace-mcp-server oauth-setup <client_id> <client_secret>
57
55
  ```
58
56
 
59
57
  You can also pass credentials via environment variables:
60
58
 
61
59
  ```bash
62
- GMAIL_OAUTH_CLIENT_ID=... GMAIL_OAUTH_CLIENT_SECRET=... npx tsx scripts/oauth-setup.ts
60
+ GMAIL_OAUTH_CLIENT_ID=... GMAIL_OAUTH_CLIENT_SECRET=... npx gmail-workspace-mcp-server oauth-setup
63
61
  ```
64
62
 
65
63
  **Port conflict?** If port 3000 is already in use, specify a different port:
66
64
 
67
65
  ```bash
68
- PORT=3001 npx tsx scripts/oauth-setup.ts <client_id> <client_secret>
66
+ PORT=3001 npx gmail-workspace-mcp-server oauth-setup <client_id> <client_secret>
69
67
  ```
70
68
 
71
69
  Desktop app credentials automatically allow `http://localhost` redirects on any port, so no additional Google Cloud Console configuration is needed.
@@ -264,17 +262,32 @@ Create a draft email, optionally as a reply to an existing conversation.
264
262
 
265
263
  - `to` (string, required): Recipient email address
266
264
  - `subject` (string, required): Email subject
267
- - `body` (string, required): Email body (plain text)
265
+ - `plaintext_body` (string): Plain text body content (at least one of plaintext_body or html_body required)
266
+ - `html_body` (string): HTML body content for rich text formatting (at least one of plaintext_body or html_body required)
267
+ - `cc` (string, optional): CC recipients
268
+ - `bcc` (string, optional): BCC recipients
268
269
  - `thread_id` (string, optional): Thread ID for replies
269
270
  - `reply_to_email_id` (string, optional): Email ID to reply to (sets References/In-Reply-To headers)
270
271
 
271
- **Example:**
272
+ At least one of `plaintext_body` or `html_body` must be provided. If both are provided, a multipart email is sent with both versions.
273
+
274
+ **Example (plain text):**
275
+
276
+ ```json
277
+ {
278
+ "to": "recipient@example.com",
279
+ "subject": "Meeting Follow-up",
280
+ "plaintext_body": "Thanks for the meeting today!"
281
+ }
282
+ ```
283
+
284
+ **Example (HTML):**
272
285
 
273
286
  ```json
274
287
  {
275
288
  "to": "recipient@example.com",
276
289
  "subject": "Meeting Follow-up",
277
- "body": "Thanks for the meeting today!"
290
+ "html_body": "<p>Thanks for the meeting today! Check out <a href=\"https://example.com/notes\">the notes</a>.</p>"
278
291
  }
279
292
  ```
280
293
 
@@ -286,18 +299,31 @@ Send an email directly or from an existing draft.
286
299
 
287
300
  - `to` (string, conditional): Recipient email (required unless sending from draft)
288
301
  - `subject` (string, conditional): Email subject (required unless sending from draft)
289
- - `body` (string, conditional): Email body (required unless sending from draft)
302
+ - `plaintext_body` (string): Plain text body content (at least one of plaintext_body or html_body required, unless sending a draft)
303
+ - `html_body` (string): HTML body content for rich text formatting (at least one of plaintext_body or html_body required, unless sending a draft)
304
+ - `cc` (string, optional): CC recipients
305
+ - `bcc` (string, optional): BCC recipients
290
306
  - `from_draft_id` (string, optional): Send an existing draft by ID
291
307
  - `thread_id` (string, optional): Thread ID for replies
292
308
  - `reply_to_email_id` (string, optional): Email ID to reply to
293
309
 
294
- **Example (new email):**
310
+ **Example (plain text email):**
311
+
312
+ ```json
313
+ {
314
+ "to": "recipient@example.com",
315
+ "subject": "Hello",
316
+ "plaintext_body": "This is a test email."
317
+ }
318
+ ```
319
+
320
+ **Example (HTML email):**
295
321
 
296
322
  ```json
297
323
  {
298
324
  "to": "recipient@example.com",
299
325
  "subject": "Hello",
300
- "body": "This is a test email."
326
+ "html_body": "<p>Check out <a href=\"https://example.com\">our website</a> for more details.</p>"
301
327
  }
302
328
  ```
303
329
 
@@ -165,17 +165,18 @@ function createMockClient() {
165
165
  return { ...email, labelIds: labels };
166
166
  },
167
167
  async createDraft(options) {
168
+ const bodyContent = options.plaintextBody || options.htmlBody || '';
168
169
  const draft = {
169
170
  id: `draft_${draftIdCounter++}`,
170
171
  message: {
171
172
  id: `msg_${messageIdCounter++}`,
172
173
  threadId: options.threadId || `thread_${messageIdCounter}`,
173
174
  labelIds: ['DRAFT'],
174
- snippet: options.body.substring(0, 100),
175
+ snippet: bodyContent.substring(0, 100),
175
176
  historyId: '12347',
176
177
  internalDate: String(Date.now()),
177
178
  payload: {
178
- mimeType: 'text/plain',
179
+ mimeType: options.htmlBody ? 'text/html' : 'text/plain',
179
180
  headers: [
180
181
  { name: 'Subject', value: options.subject },
181
182
  { name: 'From', value: 'me@example.com' },
@@ -183,8 +184,8 @@ function createMockClient() {
183
184
  { name: 'Date', value: new Date().toISOString() },
184
185
  ],
185
186
  body: {
186
- size: options.body.length,
187
- data: Buffer.from(options.body).toString('base64url'),
187
+ size: bodyContent.length,
188
+ data: Buffer.from(bodyContent).toString('base64url'),
188
189
  },
189
190
  },
190
191
  },
@@ -221,15 +222,16 @@ function createMockClient() {
221
222
  mockDrafts.splice(index, 1);
222
223
  },
223
224
  async sendMessage(options) {
225
+ const bodyContent = options.plaintextBody || options.htmlBody || '';
224
226
  const sentMessage = {
225
227
  id: `msg_${messageIdCounter++}`,
226
228
  threadId: options.threadId || `thread_${messageIdCounter}`,
227
229
  labelIds: ['SENT'],
228
- snippet: options.body.substring(0, 100),
230
+ snippet: bodyContent.substring(0, 100),
229
231
  historyId: '12348',
230
232
  internalDate: String(Date.now()),
231
233
  payload: {
232
- mimeType: 'text/plain',
234
+ mimeType: options.htmlBody ? 'text/html' : 'text/plain',
233
235
  headers: [
234
236
  { name: 'Subject', value: options.subject },
235
237
  { name: 'From', value: 'me@example.com' },
@@ -237,8 +239,8 @@ function createMockClient() {
237
239
  { name: 'Date', value: new Date().toISOString() },
238
240
  ],
239
241
  body: {
240
- size: options.body.length,
241
- data: Buffer.from(options.body).toString('base64url'),
242
+ size: bodyContent.length,
243
+ data: Buffer.from(bodyContent).toString('base64url'),
242
244
  },
243
245
  },
244
246
  };
package/build/index.js CHANGED
@@ -5,12 +5,31 @@ import { dirname, join } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { createMCPServer } from '../shared/index.js';
7
7
  import { logServerStart, logError, logWarning } from '../shared/logging.js';
8
+ import { runOAuthSetup } from './oauth-setup.js';
8
9
  // Read version from package.json
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
11
  const packageJsonPath = join(__dirname, '..', 'package.json');
11
12
  const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
12
13
  const VERSION = packageJson.version;
13
14
  // =============================================================================
15
+ // CLI SUBCOMMAND HANDLING
16
+ // =============================================================================
17
+ // Check for subcommands before env validation (e.g., "oauth-setup")
18
+ const subcommand = process.argv[2];
19
+ if (subcommand === 'oauth-setup') {
20
+ runOAuthSetup(process.argv.slice(3)).catch((error) => {
21
+ console.error('Error:', error.message);
22
+ process.exit(1);
23
+ });
24
+ }
25
+ else {
26
+ // Run the MCP server (default behavior)
27
+ main().catch((error) => {
28
+ logError('main', error);
29
+ process.exit(1);
30
+ });
31
+ }
32
+ // =============================================================================
14
33
  // ENVIRONMENT VALIDATION
15
34
  // =============================================================================
16
35
  function validateEnvironment() {
@@ -45,7 +64,7 @@ function validateEnvironment() {
45
64
  console.error(' GMAIL_OAUTH_CLIENT_SECRET: OAuth2 client secret');
46
65
  console.error(' GMAIL_OAUTH_REFRESH_TOKEN: Refresh token from one-time consent flow');
47
66
  console.error('\nRun the setup script to obtain a refresh token:');
48
- console.error(' npx tsx scripts/oauth-setup.ts <client_id> <client_secret>');
67
+ console.error(' npx gmail-workspace-mcp-server oauth-setup <client_id> <client_secret>');
49
68
  console.error('\n======================================================\n');
50
69
  process.exit(1);
51
70
  }
@@ -83,7 +102,7 @@ function validateEnvironment() {
83
102
  console.error(' GMAIL_OAUTH_CLIENT_ID: OAuth2 client ID from Google Cloud Console');
84
103
  console.error(' GMAIL_OAUTH_CLIENT_SECRET: OAuth2 client secret');
85
104
  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>`');
105
+ console.error('\n Setup: Run `npx gmail-workspace-mcp-server oauth-setup <client_id> <client_secret>`');
87
106
  console.error('\n--- Option 2: Service Account (for Google Workspace) ---');
88
107
  console.error(' GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL: Service account email address');
89
108
  console.error(' Example: my-service-account@my-project.iam.gserviceaccount.com');
@@ -123,8 +142,3 @@ async function main() {
123
142
  await server.connect(transport);
124
143
  logServerStart('Gmail');
125
144
  }
126
- // Run the server
127
- main().catch((error) => {
128
- logError('main', error);
129
- process.exit(1);
130
- });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * OAuth2 setup flow for obtaining a Gmail refresh token.
3
+ *
4
+ * This module is invoked as a CLI subcommand:
5
+ * npx gmail-workspace-mcp-server oauth-setup <client_id> <client_secret>
6
+ *
7
+ * It starts a local HTTP server, opens the Google OAuth consent flow,
8
+ * and prints the resulting refresh token for use in MCP server configuration.
9
+ */
10
+ /**
11
+ * Run the OAuth2 setup flow.
12
+ * @param args - CLI arguments after "oauth-setup" (i.e., [client_id, client_secret])
13
+ */
14
+ export declare function runOAuthSetup(args: string[]): Promise<void>;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * OAuth2 setup flow for obtaining a Gmail refresh token.
3
+ *
4
+ * This module is invoked as a CLI subcommand:
5
+ * npx gmail-workspace-mcp-server oauth-setup <client_id> <client_secret>
6
+ *
7
+ * It starts a local HTTP server, opens the Google OAuth consent flow,
8
+ * and prints the resulting refresh token for use in MCP server configuration.
9
+ */
10
+ import http from 'node:http';
11
+ import { OAuth2Client } from 'google-auth-library';
12
+ const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
13
+ const SCOPES = [
14
+ 'https://www.googleapis.com/auth/gmail.readonly',
15
+ 'https://www.googleapis.com/auth/gmail.modify',
16
+ 'https://www.googleapis.com/auth/gmail.compose',
17
+ 'https://www.googleapis.com/auth/gmail.send',
18
+ ];
19
+ function waitForCallback(port) {
20
+ return new Promise((resolve, reject) => {
21
+ const server = http.createServer((req, res) => {
22
+ if (!req.url?.startsWith('/callback')) {
23
+ res.writeHead(404);
24
+ res.end('Not found');
25
+ return;
26
+ }
27
+ const url = new URL(req.url, `http://localhost:${port}`);
28
+ const code = url.searchParams.get('code');
29
+ const error = url.searchParams.get('error');
30
+ if (error) {
31
+ res.writeHead(200, { 'Content-Type': 'text/html' });
32
+ res.end('<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>');
33
+ server.close();
34
+ reject(new Error(`OAuth error: ${error}`));
35
+ return;
36
+ }
37
+ if (!code) {
38
+ res.writeHead(400, { 'Content-Type': 'text/html' });
39
+ res.end('<html><body><h1>Missing Code</h1><p>No authorization code received.</p></body></html>');
40
+ return;
41
+ }
42
+ res.writeHead(200, { 'Content-Type': 'text/html' });
43
+ res.end('<html><body><h1>Authorization Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>');
44
+ clearTimeout(timeout);
45
+ server.close();
46
+ resolve(code);
47
+ });
48
+ const timeout = setTimeout(() => {
49
+ console.error('\nTimeout: No callback received within 5 minutes. Exiting.');
50
+ server.close();
51
+ reject(new Error('OAuth callback timeout - no response within 5 minutes'));
52
+ }, CALLBACK_TIMEOUT_MS);
53
+ server.listen(port, () => {
54
+ // Server is listening
55
+ });
56
+ server.on('error', (err) => {
57
+ clearTimeout(timeout);
58
+ if (err.code === 'EADDRINUSE') {
59
+ reject(new Error(`Port ${port} is already in use. Please free it or set PORT env var.`));
60
+ }
61
+ else {
62
+ reject(err);
63
+ }
64
+ });
65
+ });
66
+ }
67
+ /**
68
+ * Run the OAuth2 setup flow.
69
+ * @param args - CLI arguments after "oauth-setup" (i.e., [client_id, client_secret])
70
+ */
71
+ export async function runOAuthSetup(args) {
72
+ const clientId = args[0] || process.env.GMAIL_OAUTH_CLIENT_ID;
73
+ const clientSecret = args[1] || process.env.GMAIL_OAUTH_CLIENT_SECRET;
74
+ if (!clientId || !clientSecret) {
75
+ console.error('Usage: npx gmail-workspace-mcp-server oauth-setup <client_id> <client_secret>');
76
+ console.error('');
77
+ console.error('Or set environment variables:');
78
+ console.error(' GMAIL_OAUTH_CLIENT_ID=... GMAIL_OAUTH_CLIENT_SECRET=... npx gmail-workspace-mcp-server oauth-setup');
79
+ console.error('');
80
+ console.error('Get your OAuth2 credentials from:');
81
+ console.error(' https://console.cloud.google.com/apis/credentials');
82
+ process.exit(1);
83
+ }
84
+ const port = parseInt(process.env.PORT || '3000', 10);
85
+ const redirectUri = `http://localhost:${port}/callback`;
86
+ const oauth2Client = new OAuth2Client(clientId, clientSecret, redirectUri);
87
+ const authorizeUrl = oauth2Client.generateAuthUrl({
88
+ access_type: 'offline',
89
+ scope: SCOPES,
90
+ prompt: 'consent', // Force consent to ensure refresh token is returned
91
+ });
92
+ console.log('\n=== Gmail OAuth2 Setup ===\n');
93
+ console.log('1. Open this URL in your browser:\n');
94
+ console.log(` ${authorizeUrl}\n`);
95
+ console.log('2. Sign in and authorize the application');
96
+ console.log(`3. You will be redirected to localhost:${port}/callback\n`);
97
+ console.log('Waiting for callback (5 minute timeout)...\n');
98
+ const code = await waitForCallback(port);
99
+ console.log('Authorization code received! Exchanging for tokens...\n');
100
+ const { tokens } = await oauth2Client.getToken(code);
101
+ if (!tokens.refresh_token) {
102
+ console.error('ERROR: No refresh token received.');
103
+ console.error('');
104
+ console.error('This can happen if:');
105
+ console.error(' - You previously authorized this app (revoke access at https://myaccount.google.com/permissions)');
106
+ console.error(' - The OAuth consent screen is still in "Testing" mode');
107
+ console.error('');
108
+ console.error('Try revoking access and running this script again.');
109
+ process.exit(1);
110
+ }
111
+ console.log('=== Setup Complete ===\n');
112
+ console.log('Add these environment variables to your MCP server configuration:\n');
113
+ console.log(` GMAIL_OAUTH_CLIENT_ID=${clientId}`);
114
+ console.log(` GMAIL_OAUTH_CLIENT_SECRET=${clientSecret}`);
115
+ console.log(` GMAIL_OAUTH_REFRESH_TOKEN=${tokens.refresh_token}`);
116
+ console.log('');
117
+ console.log('SECURITY NOTE: Keep your refresh token secure. Anyone with this token and your');
118
+ console.log('client credentials can access your Gmail account.\n');
119
+ console.log('Example Claude Desktop config:\n');
120
+ console.log(JSON.stringify({
121
+ mcpServers: {
122
+ gmail: {
123
+ command: 'npx',
124
+ args: ['gmail-workspace-mcp-server'],
125
+ env: {
126
+ GMAIL_OAUTH_CLIENT_ID: clientId,
127
+ GMAIL_OAUTH_CLIENT_SECRET: clientSecret,
128
+ GMAIL_OAUTH_REFRESH_TOKEN: tokens.refresh_token,
129
+ },
130
+ },
131
+ },
132
+ }, null, 2));
133
+ console.log('');
134
+ process.exit(0);
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gmail-workspace-mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for Gmail integration with OAuth2 and service account support",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -13,7 +13,8 @@ interface DraftListItem {
13
13
  export declare function createDraft(baseUrl: string, headers: Record<string, string>, from: string, options: {
14
14
  to: string;
15
15
  subject: string;
16
- body: string;
16
+ plaintextBody?: string;
17
+ htmlBody?: string;
17
18
  cc?: string;
18
19
  bcc?: string;
19
20
  threadId?: string;
@@ -4,14 +4,17 @@
4
4
  export interface MimeMessageOptions {
5
5
  to: string;
6
6
  subject: string;
7
- body: string;
7
+ plaintextBody?: string;
8
+ htmlBody?: string;
8
9
  cc?: string;
9
10
  bcc?: string;
10
11
  inReplyTo?: string;
11
12
  references?: string;
12
13
  }
13
14
  /**
14
- * Builds a MIME message from email options
15
+ * Builds a MIME message from email options.
16
+ * If both plaintextBody and htmlBody are provided, creates a multipart/alternative message.
17
+ * If only one is provided, creates a single-part message with the appropriate content type.
15
18
  */
16
19
  export declare function buildMimeMessage(from: string, options: MimeMessageOptions): string;
17
20
  /**
@@ -2,7 +2,9 @@
2
2
  * MIME message utilities for building and encoding email messages
3
3
  */
4
4
  /**
5
- * Builds a MIME message from email options
5
+ * Builds a MIME message from email options.
6
+ * If both plaintextBody and htmlBody are provided, creates a multipart/alternative message.
7
+ * If only one is provided, creates a single-part message with the appropriate content type.
6
8
  */
7
9
  export function buildMimeMessage(from, options) {
8
10
  const headers = [
@@ -10,7 +12,6 @@ export function buildMimeMessage(from, options) {
10
12
  `To: ${options.to}`,
11
13
  `Subject: ${options.subject}`,
12
14
  'MIME-Version: 1.0',
13
- 'Content-Type: text/plain; charset=utf-8',
14
15
  ];
15
16
  if (options.cc) {
16
17
  headers.push(`Cc: ${options.cc}`);
@@ -24,7 +25,24 @@ export function buildMimeMessage(from, options) {
24
25
  if (options.references) {
25
26
  headers.push(`References: ${options.references}`);
26
27
  }
27
- return headers.join('\r\n') + '\r\n\r\n' + options.body;
28
+ // If both plain text and HTML are provided, use multipart/alternative
29
+ if (options.plaintextBody && options.htmlBody) {
30
+ const boundary = `boundary_${Date.now()}_${Math.random().toString(36).substring(2)}`;
31
+ headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
32
+ const parts = [
33
+ `--${boundary}\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n${options.plaintextBody}`,
34
+ `--${boundary}\r\nContent-Type: text/html; charset=utf-8\r\n\r\n${options.htmlBody}`,
35
+ `--${boundary}--`,
36
+ ];
37
+ return headers.join('\r\n') + '\r\n\r\n' + parts.join('\r\n');
38
+ }
39
+ // Single content type
40
+ if (options.htmlBody) {
41
+ headers.push('Content-Type: text/html; charset=utf-8');
42
+ return headers.join('\r\n') + '\r\n\r\n' + options.htmlBody;
43
+ }
44
+ headers.push('Content-Type: text/plain; charset=utf-8');
45
+ return headers.join('\r\n') + '\r\n\r\n' + (options.plaintextBody ?? '');
28
46
  }
29
47
  /**
30
48
  * Converts a string to base64url encoding (RFC 4648)
@@ -5,7 +5,8 @@ import type { Email } from '../../types.js';
5
5
  export declare function sendMessage(baseUrl: string, headers: Record<string, string>, from: string, options: {
6
6
  to: string;
7
7
  subject: string;
8
- body: string;
8
+ plaintextBody?: string;
9
+ htmlBody?: string;
9
10
  cc?: string;
10
11
  bcc?: string;
11
12
  threadId?: string;
@@ -50,7 +50,8 @@ export interface IGmailClient {
50
50
  createDraft(options: {
51
51
  to: string;
52
52
  subject: string;
53
- body: string;
53
+ plaintextBody?: string;
54
+ htmlBody?: string;
54
55
  cc?: string;
55
56
  bcc?: string;
56
57
  threadId?: string;
@@ -85,7 +86,8 @@ export interface IGmailClient {
85
86
  sendMessage(options: {
86
87
  to: string;
87
88
  subject: string;
88
- body: string;
89
+ plaintextBody?: string;
90
+ htmlBody?: string;
89
91
  cc?: string;
90
92
  bcc?: string;
91
93
  threadId?: string;
@@ -167,7 +169,8 @@ declare abstract class BaseGmailClient implements IGmailClient {
167
169
  createDraft(options: {
168
170
  to: string;
169
171
  subject: string;
170
- body: string;
172
+ plaintextBody?: string;
173
+ htmlBody?: string;
171
174
  cc?: string;
172
175
  bcc?: string;
173
176
  threadId?: string;
@@ -190,7 +193,8 @@ declare abstract class BaseGmailClient implements IGmailClient {
190
193
  sendMessage(options: {
191
194
  to: string;
192
195
  subject: string;
193
- body: string;
196
+ plaintextBody?: string;
197
+ htmlBody?: string;
194
198
  cc?: string;
195
199
  bcc?: string;
196
200
  threadId?: string;
@@ -1,26 +1,47 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { z } from 'zod';
3
3
  import type { ClientFactory } from '../server.js';
4
- export declare const DraftEmailSchema: z.ZodObject<{
4
+ export declare const DraftEmailSchema: z.ZodEffects<z.ZodObject<{
5
5
  to: z.ZodString;
6
6
  subject: z.ZodString;
7
- body: z.ZodString;
7
+ plaintext_body: z.ZodOptional<z.ZodString>;
8
+ html_body: z.ZodOptional<z.ZodString>;
8
9
  cc: z.ZodOptional<z.ZodString>;
9
10
  bcc: z.ZodOptional<z.ZodString>;
10
11
  thread_id: z.ZodOptional<z.ZodString>;
11
12
  reply_to_email_id: z.ZodOptional<z.ZodString>;
12
13
  }, "strip", z.ZodTypeAny, {
13
- body: string;
14
14
  to: string;
15
15
  subject: string;
16
+ plaintext_body?: string | undefined;
17
+ html_body?: string | undefined;
16
18
  cc?: string | undefined;
17
19
  bcc?: string | undefined;
18
20
  thread_id?: string | undefined;
19
21
  reply_to_email_id?: string | undefined;
20
22
  }, {
21
- body: string;
22
23
  to: string;
23
24
  subject: string;
25
+ plaintext_body?: string | undefined;
26
+ html_body?: string | undefined;
27
+ cc?: string | undefined;
28
+ bcc?: string | undefined;
29
+ thread_id?: string | undefined;
30
+ reply_to_email_id?: string | undefined;
31
+ }>, {
32
+ to: string;
33
+ subject: string;
34
+ plaintext_body?: string | undefined;
35
+ html_body?: string | undefined;
36
+ cc?: string | undefined;
37
+ bcc?: string | undefined;
38
+ thread_id?: string | undefined;
39
+ reply_to_email_id?: string | undefined;
40
+ }, {
41
+ to: string;
42
+ subject: string;
43
+ plaintext_body?: string | undefined;
44
+ html_body?: string | undefined;
24
45
  cc?: string | undefined;
25
46
  bcc?: string | undefined;
26
47
  thread_id?: string | undefined;
@@ -40,9 +61,13 @@ export declare function draftEmailTool(server: Server, clientFactory: ClientFact
40
61
  type: string;
41
62
  description: "Subject line of the email.";
42
63
  };
43
- body: {
64
+ plaintext_body: {
65
+ type: string;
66
+ description: "Plain text body content of the email. At least one of plaintext_body or html_body must be provided. If both are provided, a multipart email is sent with both versions.";
67
+ };
68
+ html_body: {
44
69
  type: string;
45
- description: "Plain text body content of the email.";
70
+ description: "HTML body content of the email for rich text formatting (links, bold, lists, etc.). At least one of plaintext_body or html_body must be provided. If both are provided, a multipart email is sent with both versions.";
46
71
  };
47
72
  cc: {
48
73
  type: string;
@@ -3,7 +3,8 @@ import { getHeader } from '../utils/email-helpers.js';
3
3
  const PARAM_DESCRIPTIONS = {
4
4
  to: 'Recipient email address(es). For multiple recipients, separate with commas.',
5
5
  subject: 'Subject line of the email.',
6
- body: 'Plain text body content of the email.',
6
+ plaintext_body: 'Plain text body content of the email. At least one of plaintext_body or html_body must be provided. If both are provided, a multipart email is sent with both versions.',
7
+ html_body: 'HTML body content of the email for rich text formatting (links, bold, lists, etc.). At least one of plaintext_body or html_body must be provided. If both are provided, a multipart email is sent with both versions.',
7
8
  cc: 'CC recipient email address(es). For multiple, separate with commas.',
8
9
  bcc: 'BCC recipient email address(es). For multiple, separate with commas.',
9
10
  thread_id: 'Thread ID to add this draft to an existing conversation. ' +
@@ -11,26 +12,37 @@ const PARAM_DESCRIPTIONS = {
11
12
  reply_to_email_id: 'Email ID to reply to. If provided, the draft will be formatted as a reply ' +
12
13
  'with proper In-Reply-To and References headers. Also requires thread_id.',
13
14
  };
14
- export const DraftEmailSchema = z.object({
15
+ export const DraftEmailSchema = z
16
+ .object({
15
17
  to: z.string().min(1).describe(PARAM_DESCRIPTIONS.to),
16
18
  subject: z.string().min(1).describe(PARAM_DESCRIPTIONS.subject),
17
- body: z.string().min(1).describe(PARAM_DESCRIPTIONS.body),
19
+ plaintext_body: z.string().min(1).optional().describe(PARAM_DESCRIPTIONS.plaintext_body),
20
+ html_body: z.string().min(1).optional().describe(PARAM_DESCRIPTIONS.html_body),
18
21
  cc: z.string().optional().describe(PARAM_DESCRIPTIONS.cc),
19
22
  bcc: z.string().optional().describe(PARAM_DESCRIPTIONS.bcc),
20
23
  thread_id: z.string().optional().describe(PARAM_DESCRIPTIONS.thread_id),
21
24
  reply_to_email_id: z.string().optional().describe(PARAM_DESCRIPTIONS.reply_to_email_id),
25
+ })
26
+ .refine((data) => {
27
+ return Boolean(data.plaintext_body) || Boolean(data.html_body);
28
+ }, {
29
+ message: 'At least one of plaintext_body or html_body must be provided.',
22
30
  });
23
31
  const TOOL_DESCRIPTION = `Create a draft email that can be reviewed and sent later.
24
32
 
25
33
  **Parameters:**
26
34
  - to: Recipient email address(es) (required)
27
35
  - subject: Email subject line (required)
28
- - body: Plain text body content (required)
36
+ - plaintext_body: Plain text body content (at least one of plaintext_body or html_body required)
37
+ - html_body: HTML body content for rich text formatting (at least one of plaintext_body or html_body required)
29
38
  - cc: CC recipients (optional)
30
39
  - bcc: BCC recipients (optional)
31
40
  - thread_id: Thread ID to reply to an existing conversation (optional)
32
41
  - reply_to_email_id: Email ID to reply to, sets proper reply headers (optional)
33
42
 
43
+ **Body content:**
44
+ At least one of plaintext_body or html_body must be provided. If both are provided, a multipart email is sent with both plain text and HTML versions. Use html_body for rich formatting like hyperlinks, bold text, or lists.
45
+
34
46
  **Creating a reply:**
35
47
  To create a draft reply to an existing email:
36
48
  1. Get the thread_id and email_id from get_email_conversation
@@ -57,9 +69,13 @@ export function draftEmailTool(server, clientFactory) {
57
69
  type: 'string',
58
70
  description: PARAM_DESCRIPTIONS.subject,
59
71
  },
60
- body: {
72
+ plaintext_body: {
73
+ type: 'string',
74
+ description: PARAM_DESCRIPTIONS.plaintext_body,
75
+ },
76
+ html_body: {
61
77
  type: 'string',
62
- description: PARAM_DESCRIPTIONS.body,
78
+ description: PARAM_DESCRIPTIONS.html_body,
63
79
  },
64
80
  cc: {
65
81
  type: 'string',
@@ -78,7 +94,7 @@ export function draftEmailTool(server, clientFactory) {
78
94
  description: PARAM_DESCRIPTIONS.reply_to_email_id,
79
95
  },
80
96
  },
81
- required: ['to', 'subject', 'body'],
97
+ required: ['to', 'subject'],
82
98
  },
83
99
  handler: async (args) => {
84
100
  try {
@@ -103,7 +119,8 @@ export function draftEmailTool(server, clientFactory) {
103
119
  const draft = await client.createDraft({
104
120
  to: parsed.to,
105
121
  subject: parsed.subject,
106
- body: parsed.body,
122
+ plaintextBody: parsed.plaintext_body,
123
+ htmlBody: parsed.html_body,
107
124
  cc: parsed.cc,
108
125
  bcc: parsed.bcc,
109
126
  threadId: parsed.thread_id,
@@ -117,6 +134,12 @@ export function draftEmailTool(server, clientFactory) {
117
134
  }
118
135
  responseText += `\n\n**To:** ${parsed.to}`;
119
136
  responseText += `\n**Subject:** ${parsed.subject}`;
137
+ const format = parsed.plaintext_body && parsed.html_body
138
+ ? 'Multipart (plain text + HTML)'
139
+ : parsed.html_body
140
+ ? 'HTML'
141
+ : 'Plain text';
142
+ responseText += `\n**Format:** ${format}`;
120
143
  if (parsed.cc) {
121
144
  responseText += `\n**CC:** ${parsed.cc}`;
122
145
  }
@@ -4,43 +4,48 @@ import type { ClientFactory } from '../server.js';
4
4
  export declare const SendEmailSchema: z.ZodEffects<z.ZodObject<{
5
5
  to: z.ZodOptional<z.ZodString>;
6
6
  subject: z.ZodOptional<z.ZodString>;
7
- body: z.ZodOptional<z.ZodString>;
7
+ plaintext_body: z.ZodOptional<z.ZodString>;
8
+ html_body: z.ZodOptional<z.ZodString>;
8
9
  cc: z.ZodOptional<z.ZodString>;
9
10
  bcc: z.ZodOptional<z.ZodString>;
10
11
  thread_id: z.ZodOptional<z.ZodString>;
11
12
  reply_to_email_id: z.ZodOptional<z.ZodString>;
12
13
  from_draft_id: z.ZodOptional<z.ZodString>;
13
14
  }, "strip", z.ZodTypeAny, {
14
- body?: string | undefined;
15
15
  to?: string | undefined;
16
16
  subject?: string | undefined;
17
+ plaintext_body?: string | undefined;
18
+ html_body?: string | undefined;
17
19
  cc?: string | undefined;
18
20
  bcc?: string | undefined;
19
21
  thread_id?: string | undefined;
20
22
  reply_to_email_id?: string | undefined;
21
23
  from_draft_id?: string | undefined;
22
24
  }, {
23
- body?: string | undefined;
24
25
  to?: string | undefined;
25
26
  subject?: string | undefined;
27
+ plaintext_body?: string | undefined;
28
+ html_body?: string | undefined;
26
29
  cc?: string | undefined;
27
30
  bcc?: string | undefined;
28
31
  thread_id?: string | undefined;
29
32
  reply_to_email_id?: string | undefined;
30
33
  from_draft_id?: string | undefined;
31
34
  }>, {
32
- body?: string | undefined;
33
35
  to?: string | undefined;
34
36
  subject?: string | undefined;
37
+ plaintext_body?: string | undefined;
38
+ html_body?: string | undefined;
35
39
  cc?: string | undefined;
36
40
  bcc?: string | undefined;
37
41
  thread_id?: string | undefined;
38
42
  reply_to_email_id?: string | undefined;
39
43
  from_draft_id?: string | undefined;
40
44
  }, {
41
- body?: string | undefined;
42
45
  to?: string | undefined;
43
46
  subject?: string | undefined;
47
+ plaintext_body?: string | undefined;
48
+ html_body?: string | undefined;
44
49
  cc?: string | undefined;
45
50
  bcc?: string | undefined;
46
51
  thread_id?: string | undefined;
@@ -61,9 +66,13 @@ export declare function sendEmailTool(server: Server, clientFactory: ClientFacto
61
66
  type: string;
62
67
  description: "Subject line of the email.";
63
68
  };
64
- body: {
69
+ plaintext_body: {
65
70
  type: string;
66
- description: "Plain text body content of the email.";
71
+ description: "Plain text body content of the email. At least one of plaintext_body or html_body must be provided (unless sending a draft). If both are provided, a multipart email is sent with both versions.";
72
+ };
73
+ html_body: {
74
+ type: string;
75
+ description: "HTML body content of the email for rich text formatting (links, bold, lists, etc.). At least one of plaintext_body or html_body must be provided (unless sending a draft). If both are provided, a multipart email is sent with both versions.";
67
76
  };
68
77
  cc: {
69
78
  type: string;
@@ -3,7 +3,8 @@ import { getHeader } from '../utils/email-helpers.js';
3
3
  const PARAM_DESCRIPTIONS = {
4
4
  to: 'Recipient email address(es). For multiple recipients, separate with commas.',
5
5
  subject: 'Subject line of the email.',
6
- body: 'Plain text body content of the email.',
6
+ plaintext_body: 'Plain text body content of the email. At least one of plaintext_body or html_body must be provided (unless sending a draft). If both are provided, a multipart email is sent with both versions.',
7
+ html_body: 'HTML body content of the email for rich text formatting (links, bold, lists, etc.). At least one of plaintext_body or html_body must be provided (unless sending a draft). If both are provided, a multipart email is sent with both versions.',
7
8
  cc: 'CC recipient email address(es). For multiple, separate with commas.',
8
9
  bcc: 'BCC recipient email address(es). For multiple, separate with commas.',
9
10
  thread_id: 'Thread ID to add this email to an existing conversation. ' +
@@ -11,13 +12,14 @@ const PARAM_DESCRIPTIONS = {
11
12
  reply_to_email_id: 'Email ID to reply to. If provided, the email will be formatted as a reply ' +
12
13
  'with proper In-Reply-To and References headers. Also requires thread_id.',
13
14
  from_draft_id: 'Draft ID to send. If provided, sends the specified draft instead of composing a new email. ' +
14
- 'When using this, other parameters (to, subject, body, etc.) are ignored.',
15
+ 'When using this, other parameters (to, subject, plaintext_body, etc.) are ignored.',
15
16
  };
16
17
  export const SendEmailSchema = z
17
18
  .object({
18
19
  to: z.string().optional().describe(PARAM_DESCRIPTIONS.to),
19
20
  subject: z.string().optional().describe(PARAM_DESCRIPTIONS.subject),
20
- body: z.string().optional().describe(PARAM_DESCRIPTIONS.body),
21
+ plaintext_body: z.string().min(1).optional().describe(PARAM_DESCRIPTIONS.plaintext_body),
22
+ html_body: z.string().min(1).optional().describe(PARAM_DESCRIPTIONS.html_body),
21
23
  cc: z.string().optional().describe(PARAM_DESCRIPTIONS.cc),
22
24
  bcc: z.string().optional().describe(PARAM_DESCRIPTIONS.bcc),
23
25
  thread_id: z.string().optional().describe(PARAM_DESCRIPTIONS.thread_id),
@@ -25,20 +27,21 @@ export const SendEmailSchema = z
25
27
  from_draft_id: z.string().optional().describe(PARAM_DESCRIPTIONS.from_draft_id),
26
28
  })
27
29
  .refine((data) => {
28
- // Either from_draft_id is provided, OR to, subject, and body are all provided
30
+ // Either from_draft_id is provided, OR to, subject, and one of plaintext_body/html_body are all provided
29
31
  if (data.from_draft_id) {
30
32
  return true;
31
33
  }
32
- return data.to && data.subject && data.body;
34
+ return data.to && data.subject && (data.plaintext_body || data.html_body);
33
35
  }, {
34
- message: 'Either provide from_draft_id to send a draft, or provide to, subject, and body to send a new email.',
36
+ message: 'Either provide from_draft_id to send a draft, or provide to, subject, and at least one of plaintext_body or html_body to send a new email.',
35
37
  });
36
38
  const TOOL_DESCRIPTION = `Send an email immediately or send a previously created draft.
37
39
 
38
40
  **Option 1: Send a new email**
39
41
  - to: Recipient email address(es) (required)
40
42
  - subject: Email subject line (required)
41
- - body: Plain text body content (required)
43
+ - plaintext_body: Plain text body content (at least one of plaintext_body or html_body required)
44
+ - html_body: HTML body content for rich text formatting (at least one of plaintext_body or html_body required)
42
45
  - cc: CC recipients (optional)
43
46
  - bcc: BCC recipients (optional)
44
47
  - thread_id: Thread ID to reply to an existing conversation (optional)
@@ -47,6 +50,9 @@ const TOOL_DESCRIPTION = `Send an email immediately or send a previously created
47
50
  **Option 2: Send a draft**
48
51
  - from_draft_id: ID of the draft to send (all other parameters are ignored)
49
52
 
53
+ **Body content:**
54
+ At least one of plaintext_body or html_body must be provided. If both are provided, a multipart email is sent with both plain text and HTML versions. Use html_body for rich formatting like hyperlinks, bold text, or lists.
55
+
50
56
  **Sending a reply:**
51
57
  To send a reply to an existing email:
52
58
  1. Get the thread_id and email_id from get_email_conversation
@@ -73,9 +79,13 @@ export function sendEmailTool(server, clientFactory) {
73
79
  type: 'string',
74
80
  description: PARAM_DESCRIPTIONS.subject,
75
81
  },
76
- body: {
82
+ plaintext_body: {
83
+ type: 'string',
84
+ description: PARAM_DESCRIPTIONS.plaintext_body,
85
+ },
86
+ html_body: {
77
87
  type: 'string',
78
- description: PARAM_DESCRIPTIONS.body,
88
+ description: PARAM_DESCRIPTIONS.html_body,
79
89
  },
80
90
  cc: {
81
91
  type: 'string',
@@ -120,7 +130,6 @@ export function sendEmailTool(server, clientFactory) {
120
130
  // TypeScript knows these are defined due to the refine check
121
131
  const to = parsed.to;
122
132
  const subject = parsed.subject;
123
- const body = parsed.body;
124
133
  let inReplyTo;
125
134
  let references;
126
135
  // If replying to an email, get the Message-ID for proper threading
@@ -140,7 +149,8 @@ export function sendEmailTool(server, clientFactory) {
140
149
  const sentEmail = await client.sendMessage({
141
150
  to,
142
151
  subject,
143
- body,
152
+ plaintextBody: parsed.plaintext_body,
153
+ htmlBody: parsed.html_body,
144
154
  cc: parsed.cc,
145
155
  bcc: parsed.bcc,
146
156
  threadId: parsed.thread_id,
@@ -153,6 +163,12 @@ export function sendEmailTool(server, clientFactory) {
153
163
  }
154
164
  responseText += `\n\n**To:** ${to}`;
155
165
  responseText += `\n**Subject:** ${subject}`;
166
+ const format = parsed.plaintext_body && parsed.html_body
167
+ ? 'Multipart (plain text + HTML)'
168
+ : parsed.html_body
169
+ ? 'HTML'
170
+ : 'Plain text';
171
+ responseText += `\n**Format:** ${format}`;
156
172
  if (parsed.cc) {
157
173
  responseText += `\n**CC:** ${parsed.cc}`;
158
174
  }