gmail-workspace-mcp-server 0.4.8 → 0.4.9
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/build/index.integration-with-mock.js +3 -0
- package/package.json +1 -1
- package/shared/server.d.ts +8 -0
- package/shared/server.js +3 -0
- package/shared/tools/get-email-conversation.js +14 -5
- package/shared/tools/list-email-conversations.js +13 -6
- package/shared/tools/search-email-conversations.js +13 -7
- package/shared/tools/send-email.js +10 -3
- package/shared/utils/email-helpers.d.ts +18 -2
- package/shared/utils/email-helpers.js +25 -3
package/package.json
CHANGED
package/shared/server.d.ts
CHANGED
|
@@ -119,6 +119,13 @@ export interface IGmailClient {
|
|
|
119
119
|
data: string;
|
|
120
120
|
size: number;
|
|
121
121
|
}>;
|
|
122
|
+
/**
|
|
123
|
+
* Get the email address of the Gmail account this client reads from.
|
|
124
|
+
* Used to construct account-scoped Gmail web URLs so that links open in
|
|
125
|
+
* the correct mailbox regardless of which accounts the reader happens to
|
|
126
|
+
* be signed into in their browser.
|
|
127
|
+
*/
|
|
128
|
+
getAccountEmail(): Promise<string>;
|
|
122
129
|
}
|
|
123
130
|
/**
|
|
124
131
|
* Service account credentials structure
|
|
@@ -231,6 +238,7 @@ declare abstract class BaseGmailClient implements IGmailClient {
|
|
|
231
238
|
data: string;
|
|
232
239
|
size: number;
|
|
233
240
|
}>;
|
|
241
|
+
getAccountEmail(): Promise<string>;
|
|
234
242
|
}
|
|
235
243
|
/**
|
|
236
244
|
* Gmail API client implementation using service account with domain-wide delegation
|
package/shared/server.js
CHANGED
|
@@ -108,6 +108,9 @@ class BaseGmailClient {
|
|
|
108
108
|
const { getAttachment } = await import('./gmail-client/lib/get-attachment.js');
|
|
109
109
|
return getAttachment(this.baseUrl, headers, messageId, attachmentId);
|
|
110
110
|
}
|
|
111
|
+
async getAccountEmail() {
|
|
112
|
+
return this.getSenderEmail();
|
|
113
|
+
}
|
|
111
114
|
}
|
|
112
115
|
/**
|
|
113
116
|
* Gmail API client implementation using service account with domain-wide delegation
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { getHeader } from '../utils/email-helpers.js';
|
|
2
|
+
import { buildGmailUrl, 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.',
|
|
@@ -25,6 +25,7 @@ Full email details including:
|
|
|
25
25
|
- Raw HTML body (when include_html is true and HTML content is available)
|
|
26
26
|
- List of attachments (if any)
|
|
27
27
|
- Labels assigned to the email
|
|
28
|
+
- Account-scoped Gmail web URL (resolves to the correct mailbox regardless of the reader's browser session)
|
|
28
29
|
|
|
29
30
|
**Use cases:**
|
|
30
31
|
- Read the full content of an email after listing conversations
|
|
@@ -143,7 +144,11 @@ function formatFullEmail(email, options) {
|
|
|
143
144
|
let output = `# Email Details
|
|
144
145
|
|
|
145
146
|
**ID:** ${email.id}
|
|
146
|
-
**Thread ID:** ${email.threadId}
|
|
147
|
+
**Thread ID:** ${email.threadId}`;
|
|
148
|
+
if (options?.accountEmail) {
|
|
149
|
+
output += `\n**Gmail URL:** ${buildGmailUrl(options.accountEmail, email.id)}`;
|
|
150
|
+
}
|
|
151
|
+
output += `
|
|
147
152
|
|
|
148
153
|
## Headers
|
|
149
154
|
**Subject:** ${subject}
|
|
@@ -210,11 +215,15 @@ export function getEmailConversationTool(server, clientFactory) {
|
|
|
210
215
|
try {
|
|
211
216
|
const parsed = GetEmailConversationSchema.parse(args ?? {});
|
|
212
217
|
const client = clientFactory();
|
|
213
|
-
const email = await
|
|
214
|
-
|
|
215
|
-
|
|
218
|
+
const [email, accountEmail] = await Promise.all([
|
|
219
|
+
client.getMessage(parsed.email_id, {
|
|
220
|
+
format: 'full',
|
|
221
|
+
}),
|
|
222
|
+
client.getAccountEmail(),
|
|
223
|
+
]);
|
|
216
224
|
const formattedEmail = formatFullEmail(email, {
|
|
217
225
|
includeHtml: parsed.include_html,
|
|
226
|
+
accountEmail,
|
|
218
227
|
});
|
|
219
228
|
return {
|
|
220
229
|
content: [
|
|
@@ -37,6 +37,7 @@ A formatted list of email conversations with:
|
|
|
37
37
|
- Sender (From)
|
|
38
38
|
- Date received
|
|
39
39
|
- Snippet preview
|
|
40
|
+
- Account-scoped Gmail web URL (resolves to the correct mailbox regardless of the reader's browser session)
|
|
40
41
|
|
|
41
42
|
**Use cases:**
|
|
42
43
|
- Check recent inbox activity
|
|
@@ -113,18 +114,24 @@ export function listEmailConversationsTool(server, clientFactory) {
|
|
|
113
114
|
],
|
|
114
115
|
};
|
|
115
116
|
}
|
|
116
|
-
// Fetch full details for each message
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
// Fetch full details for each message, plus the account email for
|
|
118
|
+
// building account-scoped Gmail URLs.
|
|
119
|
+
const [emailDetails, accountEmail] = await Promise.all([
|
|
120
|
+
Promise.all(messages.map((msg) => client.getMessage(msg.id, {
|
|
121
|
+
format: 'metadata',
|
|
122
|
+
metadataHeaders: ['Subject', 'From', 'Date'],
|
|
123
|
+
}))),
|
|
124
|
+
client.getAccountEmail(),
|
|
125
|
+
]);
|
|
121
126
|
// Sort based on sort_by parameter
|
|
122
127
|
const sortedEmails = [...emailDetails].sort((a, b) => {
|
|
123
128
|
const dateA = parseInt(a.internalDate, 10);
|
|
124
129
|
const dateB = parseInt(b.internalDate, 10);
|
|
125
130
|
return parsed.sort_by === 'recent' ? dateB - dateA : dateA - dateB;
|
|
126
131
|
});
|
|
127
|
-
const formattedEmails = sortedEmails
|
|
132
|
+
const formattedEmails = sortedEmails
|
|
133
|
+
.map((email) => formatEmail(email, accountEmail))
|
|
134
|
+
.join('\n\n---\n\n');
|
|
128
135
|
return {
|
|
129
136
|
content: [
|
|
130
137
|
{
|
|
@@ -38,7 +38,7 @@ const TOOL_DESCRIPTION = `Search email conversations using Gmail's powerful sear
|
|
|
38
38
|
- Use - to exclude: "subject:meeting -subject:canceled"
|
|
39
39
|
|
|
40
40
|
**Returns:**
|
|
41
|
-
A formatted list of matching emails with ID, Thread ID, Subject, From, Date, and
|
|
41
|
+
A formatted list of matching emails with ID, Thread ID, Subject, From, Date, snippet, and an account-scoped Gmail web URL (resolves to the correct mailbox regardless of the reader's browser session).
|
|
42
42
|
|
|
43
43
|
**Note:** Use get_email_conversation with an email ID to retrieve full message content.`;
|
|
44
44
|
export function searchEmailConversationsTool(server, clientFactory) {
|
|
@@ -79,12 +79,18 @@ export function searchEmailConversationsTool(server, clientFactory) {
|
|
|
79
79
|
],
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
-
// Fetch full details for each message
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
// Fetch full details for each message, plus the account email for
|
|
83
|
+
// building account-scoped Gmail URLs.
|
|
84
|
+
const [emailDetails, accountEmail] = await Promise.all([
|
|
85
|
+
Promise.all(messages.map((msg) => client.getMessage(msg.id, {
|
|
86
|
+
format: 'metadata',
|
|
87
|
+
metadataHeaders: ['Subject', 'From', 'Date'],
|
|
88
|
+
}))),
|
|
89
|
+
client.getAccountEmail(),
|
|
90
|
+
]);
|
|
91
|
+
const formattedEmails = emailDetails
|
|
92
|
+
.map((email) => formatEmail(email, accountEmail))
|
|
93
|
+
.join('\n\n---\n\n');
|
|
88
94
|
return {
|
|
89
95
|
content: [
|
|
90
96
|
{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { getHeader } from '../utils/email-helpers.js';
|
|
2
|
+
import { buildGmailUrl, getHeader } from '../utils/email-helpers.js';
|
|
3
3
|
import { requestConfirmation, createConfirmationSchema, readElicitationConfig, } from '@pulsemcp/mcp-elicitation';
|
|
4
4
|
const PARAM_DESCRIPTIONS = {
|
|
5
5
|
to: 'Recipient email address(es). For multiple recipients, separate with commas.',
|
|
@@ -180,11 +180,15 @@ export function sendEmailTool(server, clientFactory) {
|
|
|
180
180
|
// Option 2: Send a draft
|
|
181
181
|
if (parsed.from_draft_id) {
|
|
182
182
|
const sentEmail = await client.sendDraft(parsed.from_draft_id);
|
|
183
|
+
const accountEmail = await client.getAccountEmail();
|
|
183
184
|
return {
|
|
184
185
|
content: [
|
|
185
186
|
{
|
|
186
187
|
type: 'text',
|
|
187
|
-
text: `Draft sent successfully!\n\n**Message ID:** ${sentEmail.id}\n
|
|
188
|
+
text: `Draft sent successfully!\n\n**Message ID:** ${sentEmail.id}\n` +
|
|
189
|
+
`**Thread ID:** ${sentEmail.threadId}\n` +
|
|
190
|
+
`**Gmail URL:** ${buildGmailUrl(accountEmail, sentEmail.id)}\n\n` +
|
|
191
|
+
`The draft has been sent and removed from Drafts.`,
|
|
188
192
|
},
|
|
189
193
|
],
|
|
190
194
|
};
|
|
@@ -220,7 +224,10 @@ export function sendEmailTool(server, clientFactory) {
|
|
|
220
224
|
inReplyTo,
|
|
221
225
|
references,
|
|
222
226
|
});
|
|
223
|
-
|
|
227
|
+
const accountEmail = await client.getAccountEmail();
|
|
228
|
+
let responseText = `Email sent successfully!\n\n**Message ID:** ${sentEmail.id}\n` +
|
|
229
|
+
`**Thread ID:** ${sentEmail.threadId}\n` +
|
|
230
|
+
`**Gmail URL:** ${buildGmailUrl(accountEmail, sentEmail.id)}`;
|
|
224
231
|
if (parsed.thread_id) {
|
|
225
232
|
responseText += '\n\nThis email was sent as a reply in an existing conversation.';
|
|
226
233
|
}
|
|
@@ -4,6 +4,22 @@ import type { Email } from '../types.js';
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function getHeader(email: Email, headerName: string): string | undefined;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Builds an account-scoped Gmail web URL for a given message.
|
|
8
|
+
*
|
|
9
|
+
* Uses the `/mail/u/<account-email>/#inbox/<messageId>` path form so the link
|
|
10
|
+
* opens in the correct mailbox regardless of which accounts the reader is
|
|
11
|
+
* signed into in their browser. The user-INDEX form `/mail/u/0/` would
|
|
12
|
+
* otherwise open whichever account happens to be at index 0 in the reader's
|
|
13
|
+
* browser session, which is rarely the impersonated/OAuth account this
|
|
14
|
+
* server reads from.
|
|
15
|
+
*
|
|
16
|
+
* Gmail also accepts `?authuser=<email>` as a query-parameter fallback.
|
|
8
17
|
*/
|
|
9
|
-
export declare function
|
|
18
|
+
export declare function buildGmailUrl(accountEmail: string, messageId: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Formats an email for display in tool output.
|
|
21
|
+
*
|
|
22
|
+
* When `accountEmail` is provided, an account-scoped Gmail web URL is
|
|
23
|
+
* appended so the user can click through to the correct mailbox.
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatEmail(email: Email, accountEmail?: string): string;
|
|
@@ -6,17 +6,39 @@ export function getHeader(email, headerName) {
|
|
|
6
6
|
?.value;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Builds an account-scoped Gmail web URL for a given message.
|
|
10
|
+
*
|
|
11
|
+
* Uses the `/mail/u/<account-email>/#inbox/<messageId>` path form so the link
|
|
12
|
+
* opens in the correct mailbox regardless of which accounts the reader is
|
|
13
|
+
* signed into in their browser. The user-INDEX form `/mail/u/0/` would
|
|
14
|
+
* otherwise open whichever account happens to be at index 0 in the reader's
|
|
15
|
+
* browser session, which is rarely the impersonated/OAuth account this
|
|
16
|
+
* server reads from.
|
|
17
|
+
*
|
|
18
|
+
* Gmail also accepts `?authuser=<email>` as a query-parameter fallback.
|
|
10
19
|
*/
|
|
11
|
-
export function
|
|
20
|
+
export function buildGmailUrl(accountEmail, messageId) {
|
|
21
|
+
return `https://mail.google.com/mail/u/${encodeURIComponent(accountEmail)}/#inbox/${messageId}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Formats an email for display in tool output.
|
|
25
|
+
*
|
|
26
|
+
* When `accountEmail` is provided, an account-scoped Gmail web URL is
|
|
27
|
+
* appended so the user can click through to the correct mailbox.
|
|
28
|
+
*/
|
|
29
|
+
export function formatEmail(email, accountEmail) {
|
|
12
30
|
const subject = getHeader(email, 'Subject') || '(No Subject)';
|
|
13
31
|
const from = getHeader(email, 'From') || 'Unknown';
|
|
14
32
|
const date = getHeader(email, 'Date') || 'Unknown date';
|
|
15
33
|
const snippet = email.snippet || '';
|
|
16
|
-
|
|
34
|
+
let output = `**ID:** ${email.id}
|
|
17
35
|
**Thread ID:** ${email.threadId}
|
|
18
36
|
**Subject:** ${subject}
|
|
19
37
|
**From:** ${from}
|
|
20
38
|
**Date:** ${date}
|
|
21
39
|
**Preview:** ${snippet}`;
|
|
40
|
+
if (accountEmail) {
|
|
41
|
+
output += `\n**Gmail URL:** ${buildGmailUrl(accountEmail, email.id)}`;
|
|
42
|
+
}
|
|
43
|
+
return output;
|
|
22
44
|
}
|