gmail-workspace-mcp-server 0.0.3 → 0.0.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 +145 -15
- package/build/index.integration-with-mock.js +140 -4
- package/build/index.js +23 -6
- package/package.json +1 -1
- package/shared/gmail-client/lib/drafts.d.ts +42 -0
- package/shared/gmail-client/lib/drafts.js +81 -0
- package/shared/gmail-client/lib/mime-utils.d.ts +20 -0
- package/shared/gmail-client/lib/mime-utils.js +38 -0
- package/shared/gmail-client/lib/modify-message.d.ts +9 -0
- package/shared/gmail-client/lib/modify-message.js +20 -0
- package/shared/gmail-client/lib/send-message.d.ts +18 -0
- package/shared/gmail-client/lib/send-message.js +40 -0
- package/shared/index.d.ts +2 -2
- package/shared/index.js +2 -2
- package/shared/server.d.ts +108 -1
- package/shared/server.js +43 -3
- package/shared/tools/change-email-conversation.d.ts +66 -0
- package/shared/tools/change-email-conversation.js +148 -0
- package/shared/tools/draft-email.d.ts +79 -0
- package/shared/tools/draft-email.js +150 -0
- package/shared/tools/{get-email.d.ts → get-email-conversation.d.ts} +9 -2
- package/shared/tools/{get-email.js → get-email-conversation.js} +59 -10
- package/shared/tools/{list-recent-emails.d.ts → list-email-conversations.d.ts} +28 -13
- package/shared/tools/list-email-conversations.js +150 -0
- package/shared/tools/search-email-conversations.d.ts +45 -0
- package/shared/tools/search-email-conversations.js +110 -0
- package/shared/tools/send-email.d.ts +104 -0
- package/shared/tools/send-email.js +181 -0
- package/shared/tools.d.ts +19 -2
- package/shared/tools.js +56 -8
- package/shared/utils/email-helpers.d.ts +4 -0
- package/shared/utils/email-helpers.js +15 -0
- package/shared/tools/list-recent-emails.js +0 -133
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatEmail } from '../utils/email-helpers.js';
|
|
3
|
+
const PARAM_DESCRIPTIONS = {
|
|
4
|
+
query: 'Gmail search query. Supports all Gmail search operators. ' +
|
|
5
|
+
'Examples: "from:user@example.com", "subject:meeting", "is:unread", "has:attachment", ' +
|
|
6
|
+
'"after:2024/01/01", "before:2024/12/31", "in:inbox", "newer_than:7d".',
|
|
7
|
+
count: 'Maximum number of results to return. Default: 10. Max: 100.',
|
|
8
|
+
};
|
|
9
|
+
export const SearchEmailConversationsSchema = z.object({
|
|
10
|
+
query: z.string().min(1).describe(PARAM_DESCRIPTIONS.query),
|
|
11
|
+
count: z.number().positive().max(100).default(10).describe(PARAM_DESCRIPTIONS.count),
|
|
12
|
+
});
|
|
13
|
+
const TOOL_DESCRIPTION = `Search email conversations using Gmail's powerful search syntax.
|
|
14
|
+
|
|
15
|
+
**Parameters:**
|
|
16
|
+
- query: Gmail search query (required)
|
|
17
|
+
- count: Maximum results to return (default: 10, max: 100)
|
|
18
|
+
|
|
19
|
+
**Search operators:**
|
|
20
|
+
- from:user@example.com - Emails from specific sender
|
|
21
|
+
- to:user@example.com - Emails sent to specific recipient
|
|
22
|
+
- subject:keyword - Emails with keyword in subject
|
|
23
|
+
- is:unread / is:read - Unread or read emails
|
|
24
|
+
- is:starred - Starred emails
|
|
25
|
+
- has:attachment - Emails with attachments
|
|
26
|
+
- filename:pdf - Emails with specific attachment type
|
|
27
|
+
- after:2024/01/01 - Emails after a date
|
|
28
|
+
- before:2024/12/31 - Emails before a date
|
|
29
|
+
- newer_than:7d - Emails from the last 7 days
|
|
30
|
+
- older_than:1m - Emails older than 1 month
|
|
31
|
+
- in:inbox / in:sent / in:drafts - Emails in specific folder
|
|
32
|
+
- label:work - Emails with specific label
|
|
33
|
+
- "exact phrase" - Search for exact phrase
|
|
34
|
+
|
|
35
|
+
**Combining operators:**
|
|
36
|
+
- Use spaces to AND operators: "from:alice is:unread"
|
|
37
|
+
- Use OR for alternatives: "from:alice OR from:bob"
|
|
38
|
+
- Use - to exclude: "subject:meeting -subject:canceled"
|
|
39
|
+
|
|
40
|
+
**Returns:**
|
|
41
|
+
A formatted list of matching emails with ID, Thread ID, Subject, From, Date, and snippet.
|
|
42
|
+
|
|
43
|
+
**Note:** Use get_email_conversation with an email ID to retrieve full message content.`;
|
|
44
|
+
export function searchEmailConversationsTool(server, clientFactory) {
|
|
45
|
+
return {
|
|
46
|
+
name: 'search_email_conversations',
|
|
47
|
+
description: TOOL_DESCRIPTION,
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {
|
|
51
|
+
query: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description: PARAM_DESCRIPTIONS.query,
|
|
54
|
+
},
|
|
55
|
+
count: {
|
|
56
|
+
type: 'number',
|
|
57
|
+
default: 10,
|
|
58
|
+
description: PARAM_DESCRIPTIONS.count,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
required: ['query'],
|
|
62
|
+
},
|
|
63
|
+
handler: async (args) => {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = SearchEmailConversationsSchema.parse(args ?? {});
|
|
66
|
+
const client = clientFactory();
|
|
67
|
+
// Search messages using the query
|
|
68
|
+
const { messages } = await client.listMessages({
|
|
69
|
+
q: parsed.query,
|
|
70
|
+
maxResults: parsed.count,
|
|
71
|
+
});
|
|
72
|
+
if (messages.length === 0) {
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{
|
|
76
|
+
type: 'text',
|
|
77
|
+
text: `No emails found matching query: "${parsed.query}"`,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Fetch full details for each message
|
|
83
|
+
const emailDetails = await Promise.all(messages.map((msg) => client.getMessage(msg.id, {
|
|
84
|
+
format: 'metadata',
|
|
85
|
+
metadataHeaders: ['Subject', 'From', 'Date'],
|
|
86
|
+
})));
|
|
87
|
+
const formattedEmails = emailDetails.map(formatEmail).join('\n\n---\n\n');
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: `Found ${messages.length} email(s) matching "${parsed.query}":\n\n${formattedEmails}`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: 'text',
|
|
102
|
+
text: `Error searching emails: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
isError: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ClientFactory } from '../server.js';
|
|
4
|
+
export declare const SendEmailSchema: z.ZodEffects<z.ZodObject<{
|
|
5
|
+
to: z.ZodOptional<z.ZodString>;
|
|
6
|
+
subject: z.ZodOptional<z.ZodString>;
|
|
7
|
+
body: z.ZodOptional<z.ZodString>;
|
|
8
|
+
cc: z.ZodOptional<z.ZodString>;
|
|
9
|
+
bcc: z.ZodOptional<z.ZodString>;
|
|
10
|
+
thread_id: z.ZodOptional<z.ZodString>;
|
|
11
|
+
reply_to_email_id: z.ZodOptional<z.ZodString>;
|
|
12
|
+
from_draft_id: z.ZodOptional<z.ZodString>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
body?: string | undefined;
|
|
15
|
+
to?: string | undefined;
|
|
16
|
+
subject?: string | undefined;
|
|
17
|
+
cc?: string | undefined;
|
|
18
|
+
bcc?: string | undefined;
|
|
19
|
+
thread_id?: string | undefined;
|
|
20
|
+
reply_to_email_id?: string | undefined;
|
|
21
|
+
from_draft_id?: string | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
body?: string | undefined;
|
|
24
|
+
to?: string | undefined;
|
|
25
|
+
subject?: string | undefined;
|
|
26
|
+
cc?: string | undefined;
|
|
27
|
+
bcc?: string | undefined;
|
|
28
|
+
thread_id?: string | undefined;
|
|
29
|
+
reply_to_email_id?: string | undefined;
|
|
30
|
+
from_draft_id?: string | undefined;
|
|
31
|
+
}>, {
|
|
32
|
+
body?: string | undefined;
|
|
33
|
+
to?: string | undefined;
|
|
34
|
+
subject?: string | undefined;
|
|
35
|
+
cc?: string | undefined;
|
|
36
|
+
bcc?: string | undefined;
|
|
37
|
+
thread_id?: string | undefined;
|
|
38
|
+
reply_to_email_id?: string | undefined;
|
|
39
|
+
from_draft_id?: string | undefined;
|
|
40
|
+
}, {
|
|
41
|
+
body?: string | undefined;
|
|
42
|
+
to?: string | undefined;
|
|
43
|
+
subject?: string | undefined;
|
|
44
|
+
cc?: string | undefined;
|
|
45
|
+
bcc?: string | undefined;
|
|
46
|
+
thread_id?: string | undefined;
|
|
47
|
+
reply_to_email_id?: string | undefined;
|
|
48
|
+
from_draft_id?: string | undefined;
|
|
49
|
+
}>;
|
|
50
|
+
export declare function sendEmailTool(server: Server, clientFactory: ClientFactory): {
|
|
51
|
+
name: string;
|
|
52
|
+
description: string;
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object";
|
|
55
|
+
properties: {
|
|
56
|
+
to: {
|
|
57
|
+
type: string;
|
|
58
|
+
description: "Recipient email address(es). For multiple recipients, separate with commas.";
|
|
59
|
+
};
|
|
60
|
+
subject: {
|
|
61
|
+
type: string;
|
|
62
|
+
description: "Subject line of the email.";
|
|
63
|
+
};
|
|
64
|
+
body: {
|
|
65
|
+
type: string;
|
|
66
|
+
description: "Plain text body content of the email.";
|
|
67
|
+
};
|
|
68
|
+
cc: {
|
|
69
|
+
type: string;
|
|
70
|
+
description: "CC recipient email address(es). For multiple, separate with commas.";
|
|
71
|
+
};
|
|
72
|
+
bcc: {
|
|
73
|
+
type: string;
|
|
74
|
+
description: "BCC recipient email address(es). For multiple, separate with commas.";
|
|
75
|
+
};
|
|
76
|
+
thread_id: {
|
|
77
|
+
type: string;
|
|
78
|
+
description: string;
|
|
79
|
+
};
|
|
80
|
+
reply_to_email_id: {
|
|
81
|
+
type: string;
|
|
82
|
+
description: string;
|
|
83
|
+
};
|
|
84
|
+
from_draft_id: {
|
|
85
|
+
type: string;
|
|
86
|
+
description: string;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
required: never[];
|
|
90
|
+
};
|
|
91
|
+
handler: (args: unknown) => Promise<{
|
|
92
|
+
content: {
|
|
93
|
+
type: string;
|
|
94
|
+
text: string;
|
|
95
|
+
}[];
|
|
96
|
+
isError?: undefined;
|
|
97
|
+
} | {
|
|
98
|
+
content: {
|
|
99
|
+
type: string;
|
|
100
|
+
text: string;
|
|
101
|
+
}[];
|
|
102
|
+
isError: boolean;
|
|
103
|
+
}>;
|
|
104
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getHeader } from '../utils/email-helpers.js';
|
|
3
|
+
const PARAM_DESCRIPTIONS = {
|
|
4
|
+
to: 'Recipient email address(es). For multiple recipients, separate with commas.',
|
|
5
|
+
subject: 'Subject line of the email.',
|
|
6
|
+
body: 'Plain text body content of the email.',
|
|
7
|
+
cc: 'CC recipient email address(es). For multiple, separate with commas.',
|
|
8
|
+
bcc: 'BCC recipient email address(es). For multiple, separate with commas.',
|
|
9
|
+
thread_id: 'Thread ID to add this email to an existing conversation. ' +
|
|
10
|
+
'Get this from get_email_conversation. If provided, the email will be a reply in that thread.',
|
|
11
|
+
reply_to_email_id: 'Email ID to reply to. If provided, the email will be formatted as a reply ' +
|
|
12
|
+
'with proper In-Reply-To and References headers. Also requires thread_id.',
|
|
13
|
+
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
|
+
};
|
|
16
|
+
export const SendEmailSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
to: z.string().optional().describe(PARAM_DESCRIPTIONS.to),
|
|
19
|
+
subject: z.string().optional().describe(PARAM_DESCRIPTIONS.subject),
|
|
20
|
+
body: z.string().optional().describe(PARAM_DESCRIPTIONS.body),
|
|
21
|
+
cc: z.string().optional().describe(PARAM_DESCRIPTIONS.cc),
|
|
22
|
+
bcc: z.string().optional().describe(PARAM_DESCRIPTIONS.bcc),
|
|
23
|
+
thread_id: z.string().optional().describe(PARAM_DESCRIPTIONS.thread_id),
|
|
24
|
+
reply_to_email_id: z.string().optional().describe(PARAM_DESCRIPTIONS.reply_to_email_id),
|
|
25
|
+
from_draft_id: z.string().optional().describe(PARAM_DESCRIPTIONS.from_draft_id),
|
|
26
|
+
})
|
|
27
|
+
.refine((data) => {
|
|
28
|
+
// Either from_draft_id is provided, OR to, subject, and body are all provided
|
|
29
|
+
if (data.from_draft_id) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return data.to && data.subject && data.body;
|
|
33
|
+
}, {
|
|
34
|
+
message: 'Either provide from_draft_id to send a draft, or provide to, subject, and body to send a new email.',
|
|
35
|
+
});
|
|
36
|
+
const TOOL_DESCRIPTION = `Send an email immediately or send a previously created draft.
|
|
37
|
+
|
|
38
|
+
**Option 1: Send a new email**
|
|
39
|
+
- to: Recipient email address(es) (required)
|
|
40
|
+
- subject: Email subject line (required)
|
|
41
|
+
- body: Plain text body content (required)
|
|
42
|
+
- cc: CC recipients (optional)
|
|
43
|
+
- bcc: BCC recipients (optional)
|
|
44
|
+
- thread_id: Thread ID to reply to an existing conversation (optional)
|
|
45
|
+
- reply_to_email_id: Email ID to reply to, sets proper reply headers (optional)
|
|
46
|
+
|
|
47
|
+
**Option 2: Send a draft**
|
|
48
|
+
- from_draft_id: ID of the draft to send (all other parameters are ignored)
|
|
49
|
+
|
|
50
|
+
**Sending a reply:**
|
|
51
|
+
To send a reply to an existing email:
|
|
52
|
+
1. Get the thread_id and email_id from get_email_conversation
|
|
53
|
+
2. Provide both thread_id and reply_to_email_id parameters
|
|
54
|
+
|
|
55
|
+
**Use cases:**
|
|
56
|
+
- Send a new email immediately
|
|
57
|
+
- Reply to an existing email conversation
|
|
58
|
+
- Send a draft that was created with draft_email
|
|
59
|
+
|
|
60
|
+
**Warning:** This action sends the email immediately and cannot be undone.`;
|
|
61
|
+
export function sendEmailTool(server, clientFactory) {
|
|
62
|
+
return {
|
|
63
|
+
name: 'send_email',
|
|
64
|
+
description: TOOL_DESCRIPTION,
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
to: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: PARAM_DESCRIPTIONS.to,
|
|
71
|
+
},
|
|
72
|
+
subject: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: PARAM_DESCRIPTIONS.subject,
|
|
75
|
+
},
|
|
76
|
+
body: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: PARAM_DESCRIPTIONS.body,
|
|
79
|
+
},
|
|
80
|
+
cc: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: PARAM_DESCRIPTIONS.cc,
|
|
83
|
+
},
|
|
84
|
+
bcc: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
description: PARAM_DESCRIPTIONS.bcc,
|
|
87
|
+
},
|
|
88
|
+
thread_id: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: PARAM_DESCRIPTIONS.thread_id,
|
|
91
|
+
},
|
|
92
|
+
reply_to_email_id: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: PARAM_DESCRIPTIONS.reply_to_email_id,
|
|
95
|
+
},
|
|
96
|
+
from_draft_id: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: PARAM_DESCRIPTIONS.from_draft_id,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
required: [],
|
|
102
|
+
},
|
|
103
|
+
handler: async (args) => {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = SendEmailSchema.parse(args ?? {});
|
|
106
|
+
const client = clientFactory();
|
|
107
|
+
// Option 2: Send a draft
|
|
108
|
+
if (parsed.from_draft_id) {
|
|
109
|
+
const sentEmail = await client.sendDraft(parsed.from_draft_id);
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: 'text',
|
|
114
|
+
text: `Draft sent successfully!\n\n**Message ID:** ${sentEmail.id}\n**Thread ID:** ${sentEmail.threadId}\n\nThe draft has been sent and removed from Drafts.`,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Option 1: Send a new email
|
|
120
|
+
// TypeScript knows these are defined due to the refine check
|
|
121
|
+
const to = parsed.to;
|
|
122
|
+
const subject = parsed.subject;
|
|
123
|
+
const body = parsed.body;
|
|
124
|
+
let inReplyTo;
|
|
125
|
+
let references;
|
|
126
|
+
// If replying to an email, get the Message-ID for proper threading
|
|
127
|
+
if (parsed.reply_to_email_id && parsed.thread_id) {
|
|
128
|
+
const originalEmail = await client.getMessage(parsed.reply_to_email_id, {
|
|
129
|
+
format: 'metadata',
|
|
130
|
+
metadataHeaders: ['Message-ID', 'References'],
|
|
131
|
+
});
|
|
132
|
+
const messageId = getHeader(originalEmail, 'Message-ID');
|
|
133
|
+
const originalReferences = getHeader(originalEmail, 'References');
|
|
134
|
+
if (messageId) {
|
|
135
|
+
inReplyTo = messageId;
|
|
136
|
+
// Build references chain
|
|
137
|
+
references = originalReferences ? `${originalReferences} ${messageId}` : messageId;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const sentEmail = await client.sendMessage({
|
|
141
|
+
to,
|
|
142
|
+
subject,
|
|
143
|
+
body,
|
|
144
|
+
cc: parsed.cc,
|
|
145
|
+
bcc: parsed.bcc,
|
|
146
|
+
threadId: parsed.thread_id,
|
|
147
|
+
inReplyTo,
|
|
148
|
+
references,
|
|
149
|
+
});
|
|
150
|
+
let responseText = `Email sent successfully!\n\n**Message ID:** ${sentEmail.id}\n**Thread ID:** ${sentEmail.threadId}`;
|
|
151
|
+
if (parsed.thread_id) {
|
|
152
|
+
responseText += '\n\nThis email was sent as a reply in an existing conversation.';
|
|
153
|
+
}
|
|
154
|
+
responseText += `\n\n**To:** ${to}`;
|
|
155
|
+
responseText += `\n**Subject:** ${subject}`;
|
|
156
|
+
if (parsed.cc) {
|
|
157
|
+
responseText += `\n**CC:** ${parsed.cc}`;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: 'text',
|
|
163
|
+
text: responseText,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return {
|
|
170
|
+
content: [
|
|
171
|
+
{
|
|
172
|
+
type: 'text',
|
|
173
|
+
text: `Error sending email: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
isError: true,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
package/shared/tools.d.ts
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { ClientFactory } from './server.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Available tool groups for Gmail MCP server
|
|
5
|
+
* - readonly: Read-only operations (list, get, search emails)
|
|
6
|
+
* - readwrite: Read and write operations (includes readonly + modify, draft)
|
|
7
|
+
* - readwrite_external: External communication operations (includes readwrite + send_email)
|
|
5
8
|
*/
|
|
6
|
-
export
|
|
9
|
+
export type ToolGroup = 'readonly' | 'readwrite' | 'readwrite_external';
|
|
10
|
+
/**
|
|
11
|
+
* Parses the ENABLED_TOOLGROUPS environment variable
|
|
12
|
+
* @param enabledGroupsParam - Comma-separated list of tool groups
|
|
13
|
+
* @returns Array of valid tool groups
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseEnabledToolGroups(enabledGroupsParam?: string): ToolGroup[];
|
|
16
|
+
/**
|
|
17
|
+
* Gets all available tool group names
|
|
18
|
+
*/
|
|
19
|
+
export declare function getAvailableToolGroups(): ToolGroup[];
|
|
20
|
+
/**
|
|
21
|
+
* Creates a function to register tools with the server based on enabled groups
|
|
22
|
+
*/
|
|
23
|
+
export declare function createRegisterTools(clientFactory: ClientFactory, enabledGroups?: ToolGroup[]): (server: Server) => void;
|
|
7
24
|
/**
|
|
8
25
|
* Backward compatibility export
|
|
9
26
|
*/
|
package/shared/tools.js
CHANGED
|
@@ -1,17 +1,65 @@
|
|
|
1
1
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { listEmailConversationsTool } from './tools/list-email-conversations.js';
|
|
3
|
+
import { getEmailConversationTool } from './tools/get-email-conversation.js';
|
|
4
|
+
import { changeEmailConversationTool } from './tools/change-email-conversation.js';
|
|
5
|
+
import { draftEmailTool } from './tools/draft-email.js';
|
|
6
|
+
import { sendEmailTool } from './tools/send-email.js';
|
|
7
|
+
import { searchEmailConversationsTool } from './tools/search-email-conversations.js';
|
|
8
|
+
const ALL_TOOL_GROUPS = ['readonly', 'readwrite', 'readwrite_external'];
|
|
4
9
|
/**
|
|
5
|
-
* All available tools
|
|
10
|
+
* All available tools with their group assignments
|
|
11
|
+
*
|
|
12
|
+
* readonly: list_email_conversations, get_email_conversation, search_email_conversations
|
|
13
|
+
* readwrite: all readonly tools + change_email_conversation, draft_email
|
|
14
|
+
* readwrite_external: all readwrite tools + send_email (external communication)
|
|
6
15
|
*/
|
|
7
|
-
const ALL_TOOLS = [
|
|
16
|
+
const ALL_TOOLS = [
|
|
17
|
+
// Read-only tools (available in all groups)
|
|
18
|
+
{ factory: listEmailConversationsTool, groups: ['readonly', 'readwrite', 'readwrite_external'] },
|
|
19
|
+
{ factory: getEmailConversationTool, groups: ['readonly', 'readwrite', 'readwrite_external'] },
|
|
20
|
+
{
|
|
21
|
+
factory: searchEmailConversationsTool,
|
|
22
|
+
groups: ['readonly', 'readwrite', 'readwrite_external'],
|
|
23
|
+
},
|
|
24
|
+
// Write tools (available in readwrite and readwrite_external)
|
|
25
|
+
{ factory: changeEmailConversationTool, groups: ['readwrite', 'readwrite_external'] },
|
|
26
|
+
{ factory: draftEmailTool, groups: ['readwrite', 'readwrite_external'] },
|
|
27
|
+
// External communication tools (only in readwrite_external - most dangerous)
|
|
28
|
+
{ factory: sendEmailTool, groups: ['readwrite_external'] },
|
|
29
|
+
];
|
|
8
30
|
/**
|
|
9
|
-
*
|
|
31
|
+
* Parses the ENABLED_TOOLGROUPS environment variable
|
|
32
|
+
* @param enabledGroupsParam - Comma-separated list of tool groups
|
|
33
|
+
* @returns Array of valid tool groups
|
|
10
34
|
*/
|
|
11
|
-
export function
|
|
35
|
+
export function parseEnabledToolGroups(enabledGroupsParam) {
|
|
36
|
+
if (!enabledGroupsParam) {
|
|
37
|
+
return ALL_TOOL_GROUPS; // All groups enabled by default
|
|
38
|
+
}
|
|
39
|
+
const requestedGroups = enabledGroupsParam.split(',').map((g) => g.trim().toLowerCase());
|
|
40
|
+
const validGroups = requestedGroups.filter((g) => ALL_TOOL_GROUPS.includes(g));
|
|
41
|
+
if (validGroups.length === 0) {
|
|
42
|
+
console.error(`Warning: No valid tool groups found in "${enabledGroupsParam}". ` +
|
|
43
|
+
`Valid groups: ${ALL_TOOL_GROUPS.join(', ')}. Using all groups.`);
|
|
44
|
+
return ALL_TOOL_GROUPS;
|
|
45
|
+
}
|
|
46
|
+
return validGroups;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Gets all available tool group names
|
|
50
|
+
*/
|
|
51
|
+
export function getAvailableToolGroups() {
|
|
52
|
+
return [...ALL_TOOL_GROUPS];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Creates a function to register tools with the server based on enabled groups
|
|
56
|
+
*/
|
|
57
|
+
export function createRegisterTools(clientFactory, enabledGroups) {
|
|
58
|
+
// Parse enabled groups from environment or use provided array
|
|
59
|
+
const groups = enabledGroups || parseEnabledToolGroups(process.env.GMAIL_ENABLED_TOOLGROUPS);
|
|
12
60
|
return (server) => {
|
|
13
|
-
//
|
|
14
|
-
const tools = ALL_TOOLS.map((
|
|
61
|
+
// Filter tools by enabled groups and create instances
|
|
62
|
+
const tools = ALL_TOOLS.filter((def) => def.groups.some((g) => groups.includes(g))).map((def) => def.factory(server, clientFactory));
|
|
15
63
|
// List available tools
|
|
16
64
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
17
65
|
return {
|
|
@@ -3,3 +3,7 @@ import type { Email } from '../types.js';
|
|
|
3
3
|
* Extracts a header value from an email by header name (case-insensitive)
|
|
4
4
|
*/
|
|
5
5
|
export declare function getHeader(email: Email, headerName: string): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Formats an email for display in tool output
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatEmail(email: Email): string;
|
|
@@ -5,3 +5,18 @@ export function getHeader(email, headerName) {
|
|
|
5
5
|
return email.payload?.headers?.find((h) => h.name.toLowerCase() === headerName.toLowerCase())
|
|
6
6
|
?.value;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Formats an email for display in tool output
|
|
10
|
+
*/
|
|
11
|
+
export function formatEmail(email) {
|
|
12
|
+
const subject = getHeader(email, 'Subject') || '(No Subject)';
|
|
13
|
+
const from = getHeader(email, 'From') || 'Unknown';
|
|
14
|
+
const date = getHeader(email, 'Date') || 'Unknown date';
|
|
15
|
+
const snippet = email.snippet || '';
|
|
16
|
+
return `**ID:** ${email.id}
|
|
17
|
+
**Thread ID:** ${email.threadId}
|
|
18
|
+
**Subject:** ${subject}
|
|
19
|
+
**From:** ${from}
|
|
20
|
+
**Date:** ${date}
|
|
21
|
+
**Preview:** ${snippet}`;
|
|
22
|
+
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { getHeader } from '../utils/email-helpers.js';
|
|
3
|
-
const PARAM_DESCRIPTIONS = {
|
|
4
|
-
hours: 'Time horizon in hours to look back for emails. Default: 24. ' +
|
|
5
|
-
'Example: 48 for the last 2 days.',
|
|
6
|
-
labels: 'Comma-separated list of label IDs to filter by. Default: INBOX. ' +
|
|
7
|
-
'Common labels: INBOX, SENT, DRAFTS, SPAM, TRASH, STARRED, IMPORTANT, UNREAD.',
|
|
8
|
-
max_results: 'Maximum number of emails to return. Default: 10. Max: 100.',
|
|
9
|
-
};
|
|
10
|
-
export const ListRecentEmailsSchema = z.object({
|
|
11
|
-
hours: z.number().positive().default(24).describe(PARAM_DESCRIPTIONS.hours),
|
|
12
|
-
labels: z.string().optional().default('INBOX').describe(PARAM_DESCRIPTIONS.labels),
|
|
13
|
-
max_results: z.number().positive().max(100).default(10).describe(PARAM_DESCRIPTIONS.max_results),
|
|
14
|
-
});
|
|
15
|
-
const TOOL_DESCRIPTION = `List recent emails from Gmail within a specified time horizon.
|
|
16
|
-
|
|
17
|
-
Returns a list of recent emails with their subject, sender, date, and a snippet preview. Use get_email to retrieve the full content of a specific email.
|
|
18
|
-
|
|
19
|
-
**Parameters:**
|
|
20
|
-
- hours: How far back to look for emails (default: 24 hours)
|
|
21
|
-
- labels: Which labels/folders to search (default: INBOX)
|
|
22
|
-
- max_results: Maximum emails to return (default: 10, max: 100)
|
|
23
|
-
|
|
24
|
-
**Returns:**
|
|
25
|
-
A formatted list of emails with:
|
|
26
|
-
- Email ID (needed for get_email)
|
|
27
|
-
- Subject line
|
|
28
|
-
- Sender (From)
|
|
29
|
-
- Date received
|
|
30
|
-
- Snippet preview
|
|
31
|
-
|
|
32
|
-
**Use cases:**
|
|
33
|
-
- Check recent inbox activity
|
|
34
|
-
- Monitor for new emails in a time window
|
|
35
|
-
- List recent emails from specific labels like SENT or STARRED
|
|
36
|
-
|
|
37
|
-
**Note:** This tool only returns email metadata and snippets. Use get_email with an email ID to retrieve the full message content.`;
|
|
38
|
-
/**
|
|
39
|
-
* Formats an email for display
|
|
40
|
-
*/
|
|
41
|
-
function formatEmail(email) {
|
|
42
|
-
const subject = getHeader(email, 'Subject') || '(No Subject)';
|
|
43
|
-
const from = getHeader(email, 'From') || 'Unknown';
|
|
44
|
-
const date = getHeader(email, 'Date') || 'Unknown date';
|
|
45
|
-
const snippet = email.snippet || '';
|
|
46
|
-
return `**ID:** ${email.id}
|
|
47
|
-
**Subject:** ${subject}
|
|
48
|
-
**From:** ${from}
|
|
49
|
-
**Date:** ${date}
|
|
50
|
-
**Preview:** ${snippet}`;
|
|
51
|
-
}
|
|
52
|
-
export function listRecentEmailsTool(server, clientFactory) {
|
|
53
|
-
return {
|
|
54
|
-
name: 'gmail_list_recent_emails',
|
|
55
|
-
description: TOOL_DESCRIPTION,
|
|
56
|
-
inputSchema: {
|
|
57
|
-
type: 'object',
|
|
58
|
-
properties: {
|
|
59
|
-
hours: {
|
|
60
|
-
type: 'number',
|
|
61
|
-
default: 24,
|
|
62
|
-
description: PARAM_DESCRIPTIONS.hours,
|
|
63
|
-
},
|
|
64
|
-
labels: {
|
|
65
|
-
type: 'string',
|
|
66
|
-
default: 'INBOX',
|
|
67
|
-
description: PARAM_DESCRIPTIONS.labels,
|
|
68
|
-
},
|
|
69
|
-
max_results: {
|
|
70
|
-
type: 'number',
|
|
71
|
-
default: 10,
|
|
72
|
-
description: PARAM_DESCRIPTIONS.max_results,
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
required: [],
|
|
76
|
-
},
|
|
77
|
-
handler: async (args) => {
|
|
78
|
-
try {
|
|
79
|
-
const parsed = ListRecentEmailsSchema.parse(args ?? {});
|
|
80
|
-
const client = clientFactory();
|
|
81
|
-
// Calculate the timestamp for the time horizon
|
|
82
|
-
const now = new Date();
|
|
83
|
-
const cutoffDate = new Date(now.getTime() - parsed.hours * 60 * 60 * 1000);
|
|
84
|
-
const afterTimestamp = Math.floor(cutoffDate.getTime() / 1000);
|
|
85
|
-
// Build the Gmail query
|
|
86
|
-
const query = `after:${afterTimestamp}`;
|
|
87
|
-
// Parse labels
|
|
88
|
-
const labelIds = parsed.labels.split(',').map((l) => l.trim().toUpperCase());
|
|
89
|
-
// List messages
|
|
90
|
-
const { messages } = await client.listMessages({
|
|
91
|
-
q: query,
|
|
92
|
-
maxResults: parsed.max_results,
|
|
93
|
-
labelIds,
|
|
94
|
-
});
|
|
95
|
-
if (messages.length === 0) {
|
|
96
|
-
return {
|
|
97
|
-
content: [
|
|
98
|
-
{
|
|
99
|
-
type: 'text',
|
|
100
|
-
text: `No emails found in the last ${parsed.hours} hour(s) with labels: ${labelIds.join(', ')}`,
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
// Fetch full details for each message
|
|
106
|
-
const emailDetails = await Promise.all(messages.map((msg) => client.getMessage(msg.id, {
|
|
107
|
-
format: 'metadata',
|
|
108
|
-
metadataHeaders: ['Subject', 'From', 'Date'],
|
|
109
|
-
})));
|
|
110
|
-
const formattedEmails = emailDetails.map(formatEmail).join('\n\n---\n\n');
|
|
111
|
-
return {
|
|
112
|
-
content: [
|
|
113
|
-
{
|
|
114
|
-
type: 'text',
|
|
115
|
-
text: `Found ${messages.length} email(s) in the last ${parsed.hours} hour(s):\n\n${formattedEmails}`,
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
return {
|
|
122
|
-
content: [
|
|
123
|
-
{
|
|
124
|
-
type: 'text',
|
|
125
|
-
text: `Error listing emails: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
isError: true,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
}
|