gmail-workspace-mcp-server 0.0.3 → 0.0.4
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} +2 -2
- package/shared/tools/{get-email.js → get-email-conversation.js} +8 -8
- 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
package/README.md
CHANGED
|
@@ -4,8 +4,12 @@ An MCP (Model Context Protocol) server that provides Gmail integration for AI as
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **List
|
|
7
|
+
- **List Email Conversations**: Retrieve emails with label filtering
|
|
8
8
|
- **Get Email Details**: Fetch full email content including body and attachments info
|
|
9
|
+
- **Search Emails**: Search using Gmail's powerful query syntax
|
|
10
|
+
- **Manage Emails**: Mark as read/unread, star/unstar, archive, apply labels
|
|
11
|
+
- **Create Drafts**: Compose draft emails with reply support
|
|
12
|
+
- **Send Emails**: Send new emails or replies, directly or from drafts
|
|
9
13
|
- **Service Account Authentication**: Secure domain-wide delegation for Google Workspace organizations
|
|
10
14
|
|
|
11
15
|
## Installation
|
|
@@ -30,19 +34,49 @@ This server requires a Google Cloud service account with domain-wide delegation
|
|
|
30
34
|
2. Create or select a project
|
|
31
35
|
3. Enable the Gmail API
|
|
32
36
|
4. Create a service account with domain-wide delegation enabled
|
|
33
|
-
5. In [Google Workspace Admin Console](https://admin.google.com/), grant the service account access to the
|
|
37
|
+
5. In [Google Workspace Admin Console](https://admin.google.com/), grant the service account access to the following scopes:
|
|
38
|
+
- `https://www.googleapis.com/auth/gmail.readonly` (read emails)
|
|
39
|
+
- `https://www.googleapis.com/auth/gmail.modify` (modify labels)
|
|
40
|
+
- `https://www.googleapis.com/auth/gmail.compose` (create drafts)
|
|
41
|
+
- `https://www.googleapis.com/auth/gmail.send` (send emails)
|
|
34
42
|
6. Download the JSON key file
|
|
35
43
|
|
|
36
44
|
### Environment Variables
|
|
37
45
|
|
|
38
|
-
| Variable | Required | Description
|
|
39
|
-
| ------------------------------------ | -------- |
|
|
40
|
-
| `GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL` | Yes | Service account email address
|
|
41
|
-
| `GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY` | Yes | Service account private key (PEM format)
|
|
42
|
-
| `GMAIL_IMPERSONATE_EMAIL` | Yes | Email address to impersonate
|
|
46
|
+
| Variable | Required | Description |
|
|
47
|
+
| ------------------------------------ | -------- | ------------------------------------------------------------------- |
|
|
48
|
+
| `GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL` | Yes | Service account email address |
|
|
49
|
+
| `GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY` | Yes | Service account private key (PEM format) |
|
|
50
|
+
| `GMAIL_IMPERSONATE_EMAIL` | Yes | Email address to impersonate |
|
|
51
|
+
| `GMAIL_ENABLED_TOOLGROUPS` | No | Comma-separated list of tool groups to enable (default: all groups) |
|
|
43
52
|
|
|
44
53
|
You can find the `client_email` and `private_key` values in your service account JSON key file.
|
|
45
54
|
|
|
55
|
+
### Tool Groups
|
|
56
|
+
|
|
57
|
+
The server supports three tool groups for permission-based access control:
|
|
58
|
+
|
|
59
|
+
| Group | Tools Included | Risk Level |
|
|
60
|
+
| -------------------- | ---------------------------------------------------------------------------------- | ---------- |
|
|
61
|
+
| `readonly` | `list_email_conversations`, `get_email_conversation`, `search_email_conversations` | Low |
|
|
62
|
+
| `readwrite` | All readonly tools + `change_email_conversation`, `draft_email` | Medium |
|
|
63
|
+
| `readwrite_external` | All readwrite tools + `send_email` | High |
|
|
64
|
+
|
|
65
|
+
By default, all tool groups are enabled. To restrict access, set the `GMAIL_ENABLED_TOOLGROUPS` environment variable:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Read-only access (no write/send capabilities)
|
|
69
|
+
GMAIL_ENABLED_TOOLGROUPS=readonly
|
|
70
|
+
|
|
71
|
+
# Read and write, but no external sending
|
|
72
|
+
GMAIL_ENABLED_TOOLGROUPS=readwrite
|
|
73
|
+
|
|
74
|
+
# Full access including sending emails (default)
|
|
75
|
+
GMAIL_ENABLED_TOOLGROUPS=readwrite_external
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Security Note:** The `send_email` tool is in a separate `readwrite_external` group because it can send emails externally, which carries higher risk than internal operations like modifying labels or creating drafts.
|
|
79
|
+
|
|
46
80
|
## Configuration
|
|
47
81
|
|
|
48
82
|
### Claude Desktop
|
|
@@ -72,27 +106,30 @@ Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/
|
|
|
72
106
|
|
|
73
107
|
## Available Tools
|
|
74
108
|
|
|
75
|
-
###
|
|
109
|
+
### list_email_conversations
|
|
76
110
|
|
|
77
|
-
List
|
|
111
|
+
List email conversations from Gmail with optional filtering.
|
|
78
112
|
|
|
79
113
|
**Parameters:**
|
|
80
114
|
|
|
81
|
-
- `
|
|
82
|
-
- `labels` (string, optional): Comma-separated label IDs (default: "INBOX")
|
|
83
|
-
- `
|
|
115
|
+
- `count` (number, optional): Number of emails to return (default: 10, max: 100)
|
|
116
|
+
- `labels` (string, optional): Comma-separated label IDs to filter by (default: "INBOX")
|
|
117
|
+
- `sort_by` (string, optional): Sort order - "recent" (newest first) or "oldest" (default: recent)
|
|
118
|
+
- `after` (string, optional): Only return emails after this datetime, exclusive (ISO 8601 UTC, e.g., "2024-01-15T14:30:00Z")
|
|
119
|
+
- `before` (string, optional): Only return emails before this datetime, exclusive (ISO 8601 UTC, e.g., "2024-01-15T14:30:00Z")
|
|
84
120
|
|
|
85
121
|
**Example:**
|
|
86
122
|
|
|
87
123
|
```json
|
|
88
124
|
{
|
|
89
|
-
"
|
|
125
|
+
"count": 20,
|
|
90
126
|
"labels": "INBOX,STARRED",
|
|
91
|
-
"
|
|
127
|
+
"after": "2024-01-15T00:00:00Z",
|
|
128
|
+
"before": "2024-01-20T23:59:59Z"
|
|
92
129
|
}
|
|
93
130
|
```
|
|
94
131
|
|
|
95
|
-
###
|
|
132
|
+
### get_email_conversation
|
|
96
133
|
|
|
97
134
|
Retrieve the full content of a specific email by its ID.
|
|
98
135
|
|
|
@@ -108,6 +145,99 @@ Retrieve the full content of a specific email by its ID.
|
|
|
108
145
|
}
|
|
109
146
|
```
|
|
110
147
|
|
|
148
|
+
### search_email_conversations
|
|
149
|
+
|
|
150
|
+
Search emails using Gmail's powerful query syntax.
|
|
151
|
+
|
|
152
|
+
**Parameters:**
|
|
153
|
+
|
|
154
|
+
- `query` (string, required): Gmail search query (e.g., "from:user@example.com", "is:unread", "subject:meeting")
|
|
155
|
+
- `count` (number, optional): Maximum results to return (default: 10, max: 100)
|
|
156
|
+
|
|
157
|
+
**Example:**
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"query": "from:alice@example.com is:unread",
|
|
162
|
+
"count": 20
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### change_email_conversation
|
|
167
|
+
|
|
168
|
+
Modify email labels and status (read/unread, starred, archived).
|
|
169
|
+
|
|
170
|
+
**Parameters:**
|
|
171
|
+
|
|
172
|
+
- `email_id` (string, required): The email ID to modify
|
|
173
|
+
- `status` (string, optional): "read", "unread", or "archived"
|
|
174
|
+
- `is_starred` (boolean, optional): Star or unstar the email
|
|
175
|
+
- `labels` (string, optional): Comma-separated labels to add
|
|
176
|
+
- `remove_labels` (string, optional): Comma-separated labels to remove
|
|
177
|
+
|
|
178
|
+
**Example:**
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"email_id": "18abc123def456",
|
|
183
|
+
"status": "read",
|
|
184
|
+
"is_starred": true
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### draft_email
|
|
189
|
+
|
|
190
|
+
Create a draft email, optionally as a reply to an existing conversation.
|
|
191
|
+
|
|
192
|
+
**Parameters:**
|
|
193
|
+
|
|
194
|
+
- `to` (string, required): Recipient email address
|
|
195
|
+
- `subject` (string, required): Email subject
|
|
196
|
+
- `body` (string, required): Email body (plain text)
|
|
197
|
+
- `thread_id` (string, optional): Thread ID for replies
|
|
198
|
+
- `reply_to_email_id` (string, optional): Email ID to reply to (sets References/In-Reply-To headers)
|
|
199
|
+
|
|
200
|
+
**Example:**
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"to": "recipient@example.com",
|
|
205
|
+
"subject": "Meeting Follow-up",
|
|
206
|
+
"body": "Thanks for the meeting today!"
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### send_email
|
|
211
|
+
|
|
212
|
+
Send an email directly or from an existing draft.
|
|
213
|
+
|
|
214
|
+
**Parameters:**
|
|
215
|
+
|
|
216
|
+
- `to` (string, conditional): Recipient email (required unless sending from draft)
|
|
217
|
+
- `subject` (string, conditional): Email subject (required unless sending from draft)
|
|
218
|
+
- `body` (string, conditional): Email body (required unless sending from draft)
|
|
219
|
+
- `from_draft_id` (string, optional): Send an existing draft by ID
|
|
220
|
+
- `thread_id` (string, optional): Thread ID for replies
|
|
221
|
+
- `reply_to_email_id` (string, optional): Email ID to reply to
|
|
222
|
+
|
|
223
|
+
**Example (new email):**
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"to": "recipient@example.com",
|
|
228
|
+
"subject": "Hello",
|
|
229
|
+
"body": "This is a test email."
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Example (send draft):**
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"from_draft_id": "r123456789"
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
111
241
|
## Development
|
|
112
242
|
|
|
113
243
|
### Setup
|
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
* Used for running integration tests without real Gmail API access
|
|
5
5
|
*/
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
7
10
|
import { createMCPServer } from '../shared/index.js';
|
|
8
11
|
import { logServerStart, logError } from '../shared/logging.js';
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
// Read version from package.json
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const packageJsonPath = join(__dirname, '..', 'package.json');
|
|
15
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
16
|
+
const VERSION = packageJson.version;
|
|
12
17
|
const MOCK_EMAILS = [
|
|
13
18
|
{
|
|
14
19
|
id: 'msg_001',
|
|
@@ -24,6 +29,7 @@ const MOCK_EMAILS = [
|
|
|
24
29
|
{ name: 'From', value: 'alice@example.com' },
|
|
25
30
|
{ name: 'To', value: 'me@example.com' },
|
|
26
31
|
{ name: 'Date', value: new Date(Date.now() - 1000 * 60 * 30).toISOString() },
|
|
32
|
+
{ name: 'Message-ID', value: '<msg001@example.com>' },
|
|
27
33
|
],
|
|
28
34
|
body: {
|
|
29
35
|
size: 150,
|
|
@@ -46,6 +52,7 @@ const MOCK_EMAILS = [
|
|
|
46
52
|
{ name: 'From', value: 'calendar@example.com' },
|
|
47
53
|
{ name: 'To', value: 'me@example.com' },
|
|
48
54
|
{ name: 'Date', value: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString() },
|
|
55
|
+
{ name: 'Message-ID', value: '<msg002@example.com>' },
|
|
49
56
|
],
|
|
50
57
|
body: {
|
|
51
58
|
size: 200,
|
|
@@ -55,6 +62,9 @@ const MOCK_EMAILS = [
|
|
|
55
62
|
sizeEstimate: 2048,
|
|
56
63
|
},
|
|
57
64
|
];
|
|
65
|
+
const mockDrafts = [];
|
|
66
|
+
let draftIdCounter = 1;
|
|
67
|
+
let messageIdCounter = 100;
|
|
58
68
|
// =============================================================================
|
|
59
69
|
// MOCK CLIENT
|
|
60
70
|
// =============================================================================
|
|
@@ -66,6 +76,17 @@ function createMockClient() {
|
|
|
66
76
|
if (options?.labelIds && options.labelIds.length > 0) {
|
|
67
77
|
filtered = MOCK_EMAILS.filter((email) => options.labelIds.some((label) => email.labelIds?.includes(label)));
|
|
68
78
|
}
|
|
79
|
+
// Handle query search
|
|
80
|
+
if (options?.q) {
|
|
81
|
+
const query = options.q.toLowerCase();
|
|
82
|
+
filtered = filtered.filter((email) => {
|
|
83
|
+
const subject = email.payload?.headers?.find((h) => h.name === 'Subject')?.value?.toLowerCase() || '';
|
|
84
|
+
const from = email.payload?.headers?.find((h) => h.name === 'From')?.value?.toLowerCase() || '';
|
|
85
|
+
return (subject.includes(query) ||
|
|
86
|
+
from.includes(query) ||
|
|
87
|
+
email.snippet.toLowerCase().includes(query));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
69
90
|
// Apply maxResults
|
|
70
91
|
const maxResults = options?.maxResults ?? 10;
|
|
71
92
|
const messages = filtered.slice(0, maxResults).map((e) => ({
|
|
@@ -84,6 +105,121 @@ function createMockClient() {
|
|
|
84
105
|
}
|
|
85
106
|
return email;
|
|
86
107
|
},
|
|
108
|
+
async modifyMessage(messageId, options) {
|
|
109
|
+
const email = MOCK_EMAILS.find((e) => e.id === messageId);
|
|
110
|
+
if (!email) {
|
|
111
|
+
throw new Error(`Message not found: ${messageId}`);
|
|
112
|
+
}
|
|
113
|
+
// Apply label changes
|
|
114
|
+
let labels = [...(email.labelIds || [])];
|
|
115
|
+
if (options.removeLabelIds) {
|
|
116
|
+
labels = labels.filter((l) => !options.removeLabelIds.includes(l));
|
|
117
|
+
}
|
|
118
|
+
if (options.addLabelIds) {
|
|
119
|
+
for (const label of options.addLabelIds) {
|
|
120
|
+
if (!labels.includes(label)) {
|
|
121
|
+
labels.push(label);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Update the email in the mock array
|
|
126
|
+
email.labelIds = labels;
|
|
127
|
+
return { ...email, labelIds: labels };
|
|
128
|
+
},
|
|
129
|
+
async createDraft(options) {
|
|
130
|
+
const draft = {
|
|
131
|
+
id: `draft_${draftIdCounter++}`,
|
|
132
|
+
message: {
|
|
133
|
+
id: `msg_${messageIdCounter++}`,
|
|
134
|
+
threadId: options.threadId || `thread_${messageIdCounter}`,
|
|
135
|
+
labelIds: ['DRAFT'],
|
|
136
|
+
snippet: options.body.substring(0, 100),
|
|
137
|
+
historyId: '12347',
|
|
138
|
+
internalDate: String(Date.now()),
|
|
139
|
+
payload: {
|
|
140
|
+
mimeType: 'text/plain',
|
|
141
|
+
headers: [
|
|
142
|
+
{ name: 'Subject', value: options.subject },
|
|
143
|
+
{ name: 'From', value: 'me@example.com' },
|
|
144
|
+
{ name: 'To', value: options.to },
|
|
145
|
+
{ name: 'Date', value: new Date().toISOString() },
|
|
146
|
+
],
|
|
147
|
+
body: {
|
|
148
|
+
size: options.body.length,
|
|
149
|
+
data: Buffer.from(options.body).toString('base64url'),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
mockDrafts.push(draft);
|
|
155
|
+
return draft;
|
|
156
|
+
},
|
|
157
|
+
async getDraft(draftId) {
|
|
158
|
+
const draft = mockDrafts.find((d) => d.id === draftId);
|
|
159
|
+
if (!draft) {
|
|
160
|
+
throw new Error(`Draft not found: ${draftId}`);
|
|
161
|
+
}
|
|
162
|
+
return draft;
|
|
163
|
+
},
|
|
164
|
+
async listDrafts(options) {
|
|
165
|
+
const maxResults = options?.maxResults ?? 10;
|
|
166
|
+
const drafts = mockDrafts.slice(0, maxResults).map((d) => ({
|
|
167
|
+
id: d.id,
|
|
168
|
+
message: {
|
|
169
|
+
id: d.message.id,
|
|
170
|
+
threadId: d.message.threadId,
|
|
171
|
+
},
|
|
172
|
+
}));
|
|
173
|
+
return {
|
|
174
|
+
drafts,
|
|
175
|
+
resultSizeEstimate: drafts.length,
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
async deleteDraft(draftId) {
|
|
179
|
+
const index = mockDrafts.findIndex((d) => d.id === draftId);
|
|
180
|
+
if (index === -1) {
|
|
181
|
+
throw new Error(`Draft not found: ${draftId}`);
|
|
182
|
+
}
|
|
183
|
+
mockDrafts.splice(index, 1);
|
|
184
|
+
},
|
|
185
|
+
async sendMessage(options) {
|
|
186
|
+
const sentMessage = {
|
|
187
|
+
id: `msg_${messageIdCounter++}`,
|
|
188
|
+
threadId: options.threadId || `thread_${messageIdCounter}`,
|
|
189
|
+
labelIds: ['SENT'],
|
|
190
|
+
snippet: options.body.substring(0, 100),
|
|
191
|
+
historyId: '12348',
|
|
192
|
+
internalDate: String(Date.now()),
|
|
193
|
+
payload: {
|
|
194
|
+
mimeType: 'text/plain',
|
|
195
|
+
headers: [
|
|
196
|
+
{ name: 'Subject', value: options.subject },
|
|
197
|
+
{ name: 'From', value: 'me@example.com' },
|
|
198
|
+
{ name: 'To', value: options.to },
|
|
199
|
+
{ name: 'Date', value: new Date().toISOString() },
|
|
200
|
+
],
|
|
201
|
+
body: {
|
|
202
|
+
size: options.body.length,
|
|
203
|
+
data: Buffer.from(options.body).toString('base64url'),
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
return sentMessage;
|
|
208
|
+
},
|
|
209
|
+
async sendDraft(draftId) {
|
|
210
|
+
const draft = mockDrafts.find((d) => d.id === draftId);
|
|
211
|
+
if (!draft) {
|
|
212
|
+
throw new Error(`Draft not found: ${draftId}`);
|
|
213
|
+
}
|
|
214
|
+
// Remove from drafts
|
|
215
|
+
const index = mockDrafts.findIndex((d) => d.id === draftId);
|
|
216
|
+
mockDrafts.splice(index, 1);
|
|
217
|
+
// Return the message with SENT label
|
|
218
|
+
return {
|
|
219
|
+
...draft.message,
|
|
220
|
+
labelIds: ['SENT'],
|
|
221
|
+
};
|
|
222
|
+
},
|
|
87
223
|
};
|
|
88
224
|
}
|
|
89
225
|
// =============================================================================
|
|
@@ -91,7 +227,7 @@ function createMockClient() {
|
|
|
91
227
|
// =============================================================================
|
|
92
228
|
async function main() {
|
|
93
229
|
// Create server with mock client
|
|
94
|
-
const { server, registerHandlers } = createMCPServer();
|
|
230
|
+
const { server, registerHandlers } = createMCPServer({ version: VERSION });
|
|
95
231
|
// Register handlers with mock client factory
|
|
96
232
|
await registerHandlers(server, createMockClient);
|
|
97
233
|
// Start server with stdio transport
|
package/build/index.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
3
6
|
import { createMCPServer } from '../shared/index.js';
|
|
4
|
-
import { logServerStart, logError } from '../shared/logging.js';
|
|
7
|
+
import { logServerStart, logError, logWarning } from '../shared/logging.js';
|
|
8
|
+
// Read version from package.json
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const packageJsonPath = join(__dirname, '..', 'package.json');
|
|
11
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
12
|
+
const VERSION = packageJson.version;
|
|
5
13
|
// =============================================================================
|
|
6
14
|
// ENVIRONMENT VALIDATION
|
|
7
15
|
// =============================================================================
|
|
@@ -30,8 +38,13 @@ function validateEnvironment() {
|
|
|
30
38
|
console.error('\nSetup steps:');
|
|
31
39
|
console.error(' 1. Go to https://console.cloud.google.com/');
|
|
32
40
|
console.error(' 2. Create a service account with domain-wide delegation');
|
|
33
|
-
console.error(' 3. In Google Workspace Admin, grant
|
|
41
|
+
console.error(' 3. In Google Workspace Admin, grant required Gmail API scopes');
|
|
34
42
|
console.error(' 4. Download the JSON key file and extract client_email and private_key');
|
|
43
|
+
console.error('\nOptional environment variables:');
|
|
44
|
+
console.error(' GMAIL_ENABLED_TOOLGROUPS: Comma-separated list of tool groups to enable');
|
|
45
|
+
console.error(' Valid groups: readonly, readwrite, readwrite_external');
|
|
46
|
+
console.error(' Default: all groups enabled');
|
|
47
|
+
console.error(' Example: GMAIL_ENABLED_TOOLGROUPS=readwrite');
|
|
35
48
|
console.error('\n======================================================\n');
|
|
36
49
|
process.exit(1);
|
|
37
50
|
}
|
|
@@ -42,11 +55,15 @@ function validateEnvironment() {
|
|
|
42
55
|
async function main() {
|
|
43
56
|
// Step 1: Validate environment variables
|
|
44
57
|
validateEnvironment();
|
|
45
|
-
// Step 2:
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
// Step 2: Log tool groups if configured
|
|
59
|
+
if (process.env.GMAIL_ENABLED_TOOLGROUPS) {
|
|
60
|
+
logWarning('config', `Enabled tool groups: ${process.env.GMAIL_ENABLED_TOOLGROUPS}`);
|
|
61
|
+
}
|
|
62
|
+
// Step 3: Create server using factory
|
|
63
|
+
const { server, registerHandlers } = createMCPServer({ version: VERSION });
|
|
64
|
+
// Step 4: Register all handlers (tools)
|
|
48
65
|
await registerHandlers(server);
|
|
49
|
-
// Step
|
|
66
|
+
// Step 5: Start server with stdio transport
|
|
50
67
|
const transport = new StdioServerTransport();
|
|
51
68
|
await server.connect(transport);
|
|
52
69
|
logServerStart('Gmail');
|
package/package.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Email, EmailListItem } from '../../types.js';
|
|
2
|
+
interface Draft {
|
|
3
|
+
id: string;
|
|
4
|
+
message: Email;
|
|
5
|
+
}
|
|
6
|
+
interface DraftListItem {
|
|
7
|
+
id: string;
|
|
8
|
+
message: EmailListItem;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Creates a new draft email
|
|
12
|
+
*/
|
|
13
|
+
export declare function createDraft(baseUrl: string, headers: Record<string, string>, from: string, options: {
|
|
14
|
+
to: string;
|
|
15
|
+
subject: string;
|
|
16
|
+
body: string;
|
|
17
|
+
cc?: string;
|
|
18
|
+
bcc?: string;
|
|
19
|
+
threadId?: string;
|
|
20
|
+
inReplyTo?: string;
|
|
21
|
+
references?: string;
|
|
22
|
+
}): Promise<Draft>;
|
|
23
|
+
/**
|
|
24
|
+
* Gets a draft by ID
|
|
25
|
+
*/
|
|
26
|
+
export declare function getDraft(baseUrl: string, headers: Record<string, string>, draftId: string): Promise<Draft>;
|
|
27
|
+
/**
|
|
28
|
+
* Lists drafts
|
|
29
|
+
*/
|
|
30
|
+
export declare function listDrafts(baseUrl: string, headers: Record<string, string>, options?: {
|
|
31
|
+
maxResults?: number;
|
|
32
|
+
pageToken?: string;
|
|
33
|
+
}): Promise<{
|
|
34
|
+
drafts: DraftListItem[];
|
|
35
|
+
nextPageToken?: string;
|
|
36
|
+
resultSizeEstimate?: number;
|
|
37
|
+
}>;
|
|
38
|
+
/**
|
|
39
|
+
* Deletes a draft
|
|
40
|
+
*/
|
|
41
|
+
export declare function deleteDraft(baseUrl: string, headers: Record<string, string>, draftId: string): Promise<void>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { handleApiError } from './api-errors.js';
|
|
2
|
+
import { buildMimeMessage, toBase64Url } from './mime-utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a new draft email
|
|
5
|
+
*/
|
|
6
|
+
export async function createDraft(baseUrl, headers, from, options) {
|
|
7
|
+
const url = `${baseUrl}/drafts`;
|
|
8
|
+
const rawMessage = buildMimeMessage(from, options);
|
|
9
|
+
const encodedMessage = toBase64Url(rawMessage);
|
|
10
|
+
const requestBody = {
|
|
11
|
+
message: {
|
|
12
|
+
raw: encodedMessage,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
if (options.threadId) {
|
|
16
|
+
requestBody.message.threadId = options.threadId;
|
|
17
|
+
}
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers,
|
|
21
|
+
body: JSON.stringify(requestBody),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
handleApiError(response.status, 'creating draft');
|
|
25
|
+
}
|
|
26
|
+
return (await response.json());
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Gets a draft by ID
|
|
30
|
+
*/
|
|
31
|
+
export async function getDraft(baseUrl, headers, draftId) {
|
|
32
|
+
const url = `${baseUrl}/drafts/${draftId}`;
|
|
33
|
+
const response = await fetch(url, {
|
|
34
|
+
method: 'GET',
|
|
35
|
+
headers,
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
handleApiError(response.status, 'getting draft', draftId);
|
|
39
|
+
}
|
|
40
|
+
return (await response.json());
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Lists drafts
|
|
44
|
+
*/
|
|
45
|
+
export async function listDrafts(baseUrl, headers, options) {
|
|
46
|
+
const params = new URLSearchParams();
|
|
47
|
+
if (options?.maxResults) {
|
|
48
|
+
params.set('maxResults', options.maxResults.toString());
|
|
49
|
+
}
|
|
50
|
+
if (options?.pageToken) {
|
|
51
|
+
params.set('pageToken', options.pageToken);
|
|
52
|
+
}
|
|
53
|
+
const queryString = params.toString();
|
|
54
|
+
const url = `${baseUrl}/drafts${queryString ? `?${queryString}` : ''}`;
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
method: 'GET',
|
|
57
|
+
headers,
|
|
58
|
+
});
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
handleApiError(response.status, 'listing drafts');
|
|
61
|
+
}
|
|
62
|
+
const data = (await response.json());
|
|
63
|
+
return {
|
|
64
|
+
drafts: data.drafts ?? [],
|
|
65
|
+
nextPageToken: data.nextPageToken,
|
|
66
|
+
resultSizeEstimate: data.resultSizeEstimate,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Deletes a draft
|
|
71
|
+
*/
|
|
72
|
+
export async function deleteDraft(baseUrl, headers, draftId) {
|
|
73
|
+
const url = `${baseUrl}/drafts/${draftId}`;
|
|
74
|
+
const response = await fetch(url, {
|
|
75
|
+
method: 'DELETE',
|
|
76
|
+
headers,
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
handleApiError(response.status, 'deleting draft', draftId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIME message utilities for building and encoding email messages
|
|
3
|
+
*/
|
|
4
|
+
export interface MimeMessageOptions {
|
|
5
|
+
to: string;
|
|
6
|
+
subject: string;
|
|
7
|
+
body: string;
|
|
8
|
+
cc?: string;
|
|
9
|
+
bcc?: string;
|
|
10
|
+
inReplyTo?: string;
|
|
11
|
+
references?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Builds a MIME message from email options
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildMimeMessage(from: string, options: MimeMessageOptions): string;
|
|
17
|
+
/**
|
|
18
|
+
* Converts a string to base64url encoding (RFC 4648)
|
|
19
|
+
*/
|
|
20
|
+
export declare function toBase64Url(str: string): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIME message utilities for building and encoding email messages
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Builds a MIME message from email options
|
|
6
|
+
*/
|
|
7
|
+
export function buildMimeMessage(from, options) {
|
|
8
|
+
const headers = [
|
|
9
|
+
`From: ${from}`,
|
|
10
|
+
`To: ${options.to}`,
|
|
11
|
+
`Subject: ${options.subject}`,
|
|
12
|
+
'MIME-Version: 1.0',
|
|
13
|
+
'Content-Type: text/plain; charset=utf-8',
|
|
14
|
+
];
|
|
15
|
+
if (options.cc) {
|
|
16
|
+
headers.push(`Cc: ${options.cc}`);
|
|
17
|
+
}
|
|
18
|
+
if (options.bcc) {
|
|
19
|
+
headers.push(`Bcc: ${options.bcc}`);
|
|
20
|
+
}
|
|
21
|
+
if (options.inReplyTo) {
|
|
22
|
+
headers.push(`In-Reply-To: ${options.inReplyTo}`);
|
|
23
|
+
}
|
|
24
|
+
if (options.references) {
|
|
25
|
+
headers.push(`References: ${options.references}`);
|
|
26
|
+
}
|
|
27
|
+
return headers.join('\r\n') + '\r\n\r\n' + options.body;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Converts a string to base64url encoding (RFC 4648)
|
|
31
|
+
*/
|
|
32
|
+
export function toBase64Url(str) {
|
|
33
|
+
return Buffer.from(str, 'utf-8')
|
|
34
|
+
.toString('base64')
|
|
35
|
+
.replace(/\+/g, '-')
|
|
36
|
+
.replace(/\//g, '_')
|
|
37
|
+
.replace(/=+$/, '');
|
|
38
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Email } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Modifies labels on a message
|
|
4
|
+
* Used for marking read/unread, starring, archiving, etc.
|
|
5
|
+
*/
|
|
6
|
+
export declare function modifyMessage(baseUrl: string, headers: Record<string, string>, messageId: string, options: {
|
|
7
|
+
addLabelIds?: string[];
|
|
8
|
+
removeLabelIds?: string[];
|
|
9
|
+
}): Promise<Email>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { handleApiError } from './api-errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Modifies labels on a message
|
|
4
|
+
* Used for marking read/unread, starring, archiving, etc.
|
|
5
|
+
*/
|
|
6
|
+
export async function modifyMessage(baseUrl, headers, messageId, options) {
|
|
7
|
+
const url = `${baseUrl}/messages/${messageId}/modify`;
|
|
8
|
+
const response = await fetch(url, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers,
|
|
11
|
+
body: JSON.stringify({
|
|
12
|
+
addLabelIds: options.addLabelIds || [],
|
|
13
|
+
removeLabelIds: options.removeLabelIds || [],
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
handleApiError(response.status, 'modifying message', messageId);
|
|
18
|
+
}
|
|
19
|
+
return (await response.json());
|
|
20
|
+
}
|