gmail-workspace-mcp-server 0.4.3 → 0.4.5

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
@@ -273,21 +273,22 @@ List draft emails from Gmail with optional thread filtering.
273
273
 
274
274
  ### upsert_draft_email
275
275
 
276
- Create a new draft email or update an existing one. Optionally as a reply to an existing conversation.
276
+ Create, update, or delete a draft email. Optionally as a reply to an existing conversation.
277
277
 
278
278
  **Parameters:**
279
279
 
280
- - `draft_id` (string, optional): ID of an existing draft to update (omit to create a new draft)
281
- - `to` (string, required): Recipient email address
282
- - `subject` (string, required): Email subject
283
- - `plaintext_body` (string): Plain text body content (at least one of plaintext_body or html_body required)
284
- - `html_body` (string): HTML body content for rich text formatting (at least one of plaintext_body or html_body required)
280
+ - `draft_id` (string, optional): ID of an existing draft to update or delete (omit to create a new draft)
281
+ - `delete` (boolean, optional): Set to `true` to delete the draft specified by `draft_id`. All other parameters are ignored when deleting
282
+ - `to` (string, required for create/update): Recipient email address
283
+ - `subject` (string, required for create/update): Email subject
284
+ - `plaintext_body` (string): Plain text body content (at least one of plaintext_body or html_body required for create/update)
285
+ - `html_body` (string): HTML body content for rich text formatting (at least one of plaintext_body or html_body required for create/update)
285
286
  - `cc` (string, optional): CC recipients
286
287
  - `bcc` (string, optional): BCC recipients
287
288
  - `thread_id` (string, optional): Thread ID for replies
288
289
  - `reply_to_email_id` (string, optional): Email ID to reply to (sets References/In-Reply-To headers)
289
290
 
290
- At least one of `plaintext_body` or `html_body` must be provided. If both are provided, a multipart email is sent with both versions.
291
+ For create/update: at least one of `plaintext_body` or `html_body` must be provided. If both are provided, a multipart email is sent with both versions.
291
292
 
292
293
  **Example (create new draft):**
293
294
 
@@ -310,6 +311,15 @@ At least one of `plaintext_body` or `html_body` must be provided. If both are pr
310
311
  }
311
312
  ```
312
313
 
314
+ **Example (delete a draft):**
315
+
316
+ ```json
317
+ {
318
+ "draft_id": "r123456789",
319
+ "delete": true
320
+ }
321
+ ```
322
+
313
323
  ### send_email
314
324
 
315
325
  Send an email directly or from an existing draft.
@@ -1,5 +1,11 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { readElicitationConfig } from './config.js';
3
+ /**
4
+ * The set of action values recognized by the elicitation protocol.
5
+ * Includes 'pending' for completeness, though it is filtered before
6
+ * reaching the validation check in pollElicitationStatus.
7
+ */
8
+ const VALID_ELICITATION_ACTIONS = new Set(['pending', 'accept', 'decline', 'cancel', 'expired']);
3
9
  /**
4
10
  * Checks whether the connected client supports native form elicitation.
5
11
  */
@@ -22,6 +28,14 @@ async function nativeElicit(server, message, requestedSchema) {
22
28
  requestedSchema,
23
29
  };
24
30
  const result = await server.elicitInput(params);
31
+ // Fail-safe: validate the action even from native elicitation.
32
+ // The TypeScript type says 'accept' | 'decline' | 'cancel', but at runtime
33
+ // the MCP client could return any string over the wire.
34
+ if (!VALID_ELICITATION_ACTIONS.has(result.action)) {
35
+ console.warn(`[elicitation] Unrecognized native elicitation action "${result.action}". ` +
36
+ `Treating as "decline" (fail-safe).`);
37
+ return { action: 'decline' };
38
+ }
25
39
  return {
26
40
  action: result.action,
27
41
  content: result.content ?? undefined,
@@ -66,6 +80,14 @@ async function pollElicitationStatus(config, requestId, expiresAt) {
66
80
  }
67
81
  const data = (await response.json());
68
82
  if (data.action !== 'pending') {
83
+ // Fail-safe: only allow recognized action values through.
84
+ // Unrecognized actions are treated as 'decline' to prevent
85
+ // unintended execution of protected operations.
86
+ if (!VALID_ELICITATION_ACTIONS.has(data.action)) {
87
+ console.warn(`[elicitation] Unrecognized poll action "${data.action}" for request ${requestId}. ` +
88
+ `Treating as "decline" (fail-safe).`);
89
+ return { action: 'decline' };
90
+ }
69
91
  return {
70
92
  action: data.action,
71
93
  content: data.content ?? undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gmail-workspace-mcp-server",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "MCP server for Gmail integration with OAuth2 and service account support",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -3,8 +3,9 @@ import { z } from 'zod';
3
3
  import type { ClientFactory } from '../server.js';
4
4
  export declare const UpsertDraftEmailSchema: z.ZodEffects<z.ZodObject<{
5
5
  draft_id: z.ZodOptional<z.ZodString>;
6
- to: z.ZodString;
7
- subject: z.ZodString;
6
+ delete: z.ZodOptional<z.ZodBoolean>;
7
+ to: z.ZodOptional<z.ZodString>;
8
+ subject: z.ZodOptional<z.ZodString>;
8
9
  plaintext_body: z.ZodOptional<z.ZodString>;
9
10
  html_body: z.ZodOptional<z.ZodString>;
10
11
  cc: z.ZodOptional<z.ZodString>;
@@ -12,9 +13,10 @@ export declare const UpsertDraftEmailSchema: z.ZodEffects<z.ZodObject<{
12
13
  thread_id: z.ZodOptional<z.ZodString>;
13
14
  reply_to_email_id: z.ZodOptional<z.ZodString>;
14
15
  }, "strip", z.ZodTypeAny, {
15
- to: string;
16
- subject: string;
17
16
  draft_id?: string | undefined;
17
+ delete?: boolean | undefined;
18
+ to?: string | undefined;
19
+ subject?: string | undefined;
18
20
  plaintext_body?: string | undefined;
19
21
  html_body?: string | undefined;
20
22
  cc?: string | undefined;
@@ -22,9 +24,10 @@ export declare const UpsertDraftEmailSchema: z.ZodEffects<z.ZodObject<{
22
24
  thread_id?: string | undefined;
23
25
  reply_to_email_id?: string | undefined;
24
26
  }, {
25
- to: string;
26
- subject: string;
27
27
  draft_id?: string | undefined;
28
+ delete?: boolean | undefined;
29
+ to?: string | undefined;
30
+ subject?: string | undefined;
28
31
  plaintext_body?: string | undefined;
29
32
  html_body?: string | undefined;
30
33
  cc?: string | undefined;
@@ -32,9 +35,10 @@ export declare const UpsertDraftEmailSchema: z.ZodEffects<z.ZodObject<{
32
35
  thread_id?: string | undefined;
33
36
  reply_to_email_id?: string | undefined;
34
37
  }>, {
35
- to: string;
36
- subject: string;
37
38
  draft_id?: string | undefined;
39
+ delete?: boolean | undefined;
40
+ to?: string | undefined;
41
+ subject?: string | undefined;
38
42
  plaintext_body?: string | undefined;
39
43
  html_body?: string | undefined;
40
44
  cc?: string | undefined;
@@ -42,9 +46,10 @@ export declare const UpsertDraftEmailSchema: z.ZodEffects<z.ZodObject<{
42
46
  thread_id?: string | undefined;
43
47
  reply_to_email_id?: string | undefined;
44
48
  }, {
45
- to: string;
46
- subject: string;
47
49
  draft_id?: string | undefined;
50
+ delete?: boolean | undefined;
51
+ to?: string | undefined;
52
+ subject?: string | undefined;
48
53
  plaintext_body?: string | undefined;
49
54
  html_body?: string | undefined;
50
55
  cc?: string | undefined;
@@ -62,6 +67,10 @@ export declare function upsertDraftEmailTool(server: Server, clientFactory: Clie
62
67
  type: string;
63
68
  description: string;
64
69
  };
70
+ delete: {
71
+ type: string;
72
+ description: string;
73
+ };
65
74
  to: {
66
75
  type: string;
67
76
  description: "Recipient email address(es). For multiple recipients, separate with commas.";
@@ -95,7 +104,7 @@ export declare function upsertDraftEmailTool(server: Server, clientFactory: Clie
95
104
  description: string;
96
105
  };
97
106
  };
98
- required: string[];
107
+ required: never[];
99
108
  };
100
109
  handler: (args: unknown) => Promise<{
101
110
  content: {
@@ -1,9 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  import { getHeader } from '../utils/email-helpers.js';
3
3
  const PARAM_DESCRIPTIONS = {
4
- draft_id: 'ID of an existing draft to update. If provided, the draft is replaced in-place ' +
5
- 'with the new content. If omitted, a new draft is created. ' +
4
+ draft_id: 'ID of an existing draft to update or delete. If provided, the draft is replaced in-place ' +
5
+ 'with the new content (or deleted if delete is true). If omitted, a new draft is created. ' +
6
6
  'Get draft IDs from list_draft_emails or from a previous upsert_draft_email response.',
7
+ delete: 'Set to true to delete the draft specified by draft_id. ' +
8
+ 'Requires draft_id. All other parameters are ignored when deleting.',
7
9
  to: 'Recipient email address(es). For multiple recipients, separate with commas.',
8
10
  subject: 'Subject line of the email.',
9
11
  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.',
@@ -18,8 +20,9 @@ const PARAM_DESCRIPTIONS = {
18
20
  export const UpsertDraftEmailSchema = z
19
21
  .object({
20
22
  draft_id: z.string().optional().describe(PARAM_DESCRIPTIONS.draft_id),
21
- to: z.string().min(1).describe(PARAM_DESCRIPTIONS.to),
22
- subject: z.string().min(1).describe(PARAM_DESCRIPTIONS.subject),
23
+ delete: z.boolean().optional().describe(PARAM_DESCRIPTIONS.delete),
24
+ to: z.string().min(1).optional().describe(PARAM_DESCRIPTIONS.to),
25
+ subject: z.string().min(1).optional().describe(PARAM_DESCRIPTIONS.subject),
23
26
  plaintext_body: z.string().min(1).optional().describe(PARAM_DESCRIPTIONS.plaintext_body),
24
27
  html_body: z.string().min(1).optional().describe(PARAM_DESCRIPTIONS.html_body),
25
28
  cc: z.string().optional().describe(PARAM_DESCRIPTIONS.cc),
@@ -27,26 +30,39 @@ export const UpsertDraftEmailSchema = z
27
30
  thread_id: z.string().optional().describe(PARAM_DESCRIPTIONS.thread_id),
28
31
  reply_to_email_id: z.string().optional().describe(PARAM_DESCRIPTIONS.reply_to_email_id),
29
32
  })
30
- .refine((data) => {
31
- return Boolean(data.plaintext_body) || Boolean(data.html_body);
32
- }, {
33
- message: 'At least one of plaintext_body or html_body must be provided.',
33
+ .superRefine((data, ctx) => {
34
+ if (data.delete) {
35
+ if (!data.draft_id) {
36
+ ctx.addIssue({
37
+ code: z.ZodIssueCode.custom,
38
+ message: 'draft_id is required when delete is true.',
39
+ });
40
+ }
41
+ return;
42
+ }
43
+ if (!data.to || !data.subject || (!data.plaintext_body && !data.html_body)) {
44
+ ctx.addIssue({
45
+ code: z.ZodIssueCode.custom,
46
+ message: 'to, subject, and at least one of plaintext_body or html_body must be provided.',
47
+ });
48
+ }
34
49
  });
35
- const TOOL_DESCRIPTION = `Create a new draft email or update an existing one.
50
+ const TOOL_DESCRIPTION = `Create, update, or delete a draft email.
36
51
 
37
52
  **Parameters:**
38
- - draft_id: ID of an existing draft to update (optional — omit to create a new draft)
39
- - to: Recipient email address(es) (required)
40
- - subject: Email subject line (required)
41
- - plaintext_body: Plain text body content (at least one of plaintext_body or html_body required)
42
- - html_body: HTML body content for rich text formatting (at least one of plaintext_body or html_body required)
53
+ - draft_id: ID of an existing draft to update or delete (optional — omit to create a new draft)
54
+ - delete: Set to true to delete the draft specified by draft_id (optional)
55
+ - to: Recipient email address(es) (required for create/update)
56
+ - subject: Email subject line (required for create/update)
57
+ - plaintext_body: Plain text body content (at least one of plaintext_body or html_body required for create/update)
58
+ - html_body: HTML body content for rich text formatting (at least one of plaintext_body or html_body required for create/update)
43
59
  - cc: CC recipients (optional)
44
60
  - bcc: BCC recipients (optional)
45
61
  - thread_id: Thread ID to reply to an existing conversation (optional)
46
62
  - reply_to_email_id: Email ID to reply to, sets proper reply headers (optional)
47
63
 
48
64
  **Body content:**
49
- 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.
65
+ At least one of plaintext_body or html_body must be provided for create/update. 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.
50
66
 
51
67
  **Creating a reply:**
52
68
  To create a draft reply to an existing email:
@@ -56,11 +72,15 @@ To create a draft reply to an existing email:
56
72
  **Updating a draft:**
57
73
  To update an existing draft, provide the draft_id from a previous upsert_draft_email response or from list_draft_emails. The draft is replaced in-place — all fields must be provided (not just the ones you want to change).
58
74
 
75
+ **Deleting a draft:**
76
+ To delete a draft, provide the draft_id and set delete to true. All other parameters are ignored when deleting.
77
+
59
78
  **Use cases:**
60
79
  - Draft a new email for later review
61
80
  - Prepare a reply to an email conversation
62
81
  - Revise a draft after user feedback (without creating duplicates)
63
82
  - Save an email without sending it immediately
83
+ - Delete a corrupted or unwanted draft
64
84
 
65
85
  **Note:** The draft will be saved in Gmail's Drafts folder. Use send_email with from_draft_id to send it.`;
66
86
  export function upsertDraftEmailTool(server, clientFactory) {
@@ -74,6 +94,10 @@ export function upsertDraftEmailTool(server, clientFactory) {
74
94
  type: 'string',
75
95
  description: PARAM_DESCRIPTIONS.draft_id,
76
96
  },
97
+ delete: {
98
+ type: 'boolean',
99
+ description: PARAM_DESCRIPTIONS.delete,
100
+ },
77
101
  to: {
78
102
  type: 'string',
79
103
  description: PARAM_DESCRIPTIONS.to,
@@ -107,12 +131,24 @@ export function upsertDraftEmailTool(server, clientFactory) {
107
131
  description: PARAM_DESCRIPTIONS.reply_to_email_id,
108
132
  },
109
133
  },
110
- required: ['to', 'subject'],
134
+ required: [],
111
135
  },
112
136
  handler: async (args) => {
113
137
  try {
114
138
  const parsed = UpsertDraftEmailSchema.parse(args ?? {});
115
139
  const client = clientFactory();
140
+ // Delete mode
141
+ if (parsed.delete) {
142
+ await client.deleteDraft(parsed.draft_id);
143
+ return {
144
+ content: [
145
+ {
146
+ type: 'text',
147
+ text: `Draft deleted successfully!\n\n**Draft ID:** ${parsed.draft_id}`,
148
+ },
149
+ ],
150
+ };
151
+ }
116
152
  let inReplyTo;
117
153
  let references;
118
154
  // If replying to an email, get the Message-ID for proper threading
@@ -176,11 +212,17 @@ export function upsertDraftEmailTool(server, clientFactory) {
176
212
  };
177
213
  }
178
214
  catch (error) {
215
+ const typedArgs = args;
216
+ const errorAction = typedArgs?.delete
217
+ ? 'deleting'
218
+ : typedArgs?.draft_id
219
+ ? 'updating'
220
+ : 'creating';
179
221
  return {
180
222
  content: [
181
223
  {
182
224
  type: 'text',
183
- text: `Error ${args?.draft_id ? 'updating' : 'creating'} draft: ${error instanceof Error ? error.message : 'Unknown error'}`,
225
+ text: `Error ${errorAction} draft: ${error instanceof Error ? error.message : 'Unknown error'}`,
184
226
  },
185
227
  ],
186
228
  isError: true,
@@ -102,12 +102,12 @@ export declare function sendEmailTool(server: Server, clientFactory: ClientFacto
102
102
  type: string;
103
103
  text: string;
104
104
  }[];
105
- isError?: undefined;
105
+ isError: boolean;
106
106
  } | {
107
107
  content: {
108
108
  type: string;
109
109
  text: string;
110
110
  }[];
111
- isError: boolean;
111
+ isError?: undefined;
112
112
  }>;
113
113
  };
@@ -139,7 +139,20 @@ export function sendEmailTool(server, clientFactory) {
139
139
  'com.pulsemcp/tool-name': 'send_email',
140
140
  },
141
141
  }, elicitationConfig);
142
- if (confirmation.action === 'decline' || confirmation.action === 'cancel') {
142
+ // Fail-safe: only proceed on explicit 'accept'.
143
+ // Any other action (decline, cancel, expired, or unrecognized) blocks the send.
144
+ if (confirmation.action !== 'accept') {
145
+ if (confirmation.action === 'expired') {
146
+ return {
147
+ content: [
148
+ {
149
+ type: 'text',
150
+ text: 'Email sending confirmation expired. Please try again.',
151
+ },
152
+ ],
153
+ isError: true,
154
+ };
155
+ }
143
156
  return {
144
157
  content: [
145
158
  {
@@ -149,19 +162,8 @@ export function sendEmailTool(server, clientFactory) {
149
162
  ],
150
163
  };
151
164
  }
152
- if (confirmation.action === 'expired') {
153
- return {
154
- content: [
155
- {
156
- type: 'text',
157
- text: 'Email sending confirmation expired. Please try again.',
158
- },
159
- ],
160
- isError: true,
161
- };
162
- }
163
- // action === 'accept' with content.confirm === true, or
164
- // action === 'accept' from disabled mode (no content)
165
+ // We reach here only when action === 'accept'.
166
+ // Still check if the user explicitly unchecked the confirm checkbox.
165
167
  if (confirmation.content &&
166
168
  'confirm' in confirmation.content &&
167
169
  confirmation.content.confirm === false) {