@node2flow/gmail-mcp 1.0.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.
@@ -0,0 +1,507 @@
1
+ /**
2
+ * Gmail API v1 Client — OAuth 2.0 refresh token pattern
3
+ */
4
+
5
+ import type {
6
+ Message,
7
+ MessageList,
8
+ Thread,
9
+ ThreadList,
10
+ Label,
11
+ LabelList,
12
+ Draft,
13
+ DraftList,
14
+ Profile,
15
+ VacationSettings,
16
+ Attachment,
17
+ } from './types.js';
18
+
19
+ export interface GmailClientConfig {
20
+ clientId: string;
21
+ clientSecret: string;
22
+ refreshToken: string;
23
+ }
24
+
25
+ export class GmailClient {
26
+ private config: GmailClientConfig;
27
+ private accessToken: string | null = null;
28
+ private tokenExpiry = 0;
29
+
30
+ private static readonly BASE = 'https://gmail.googleapis.com/gmail/v1';
31
+ private static readonly TOKEN_URL = 'https://oauth2.googleapis.com/token';
32
+
33
+ constructor(config: GmailClientConfig) {
34
+ this.config = config;
35
+ }
36
+
37
+ // ========== OAuth ==========
38
+
39
+ private async getAccessToken(): Promise<string> {
40
+ if (this.accessToken && Date.now() < this.tokenExpiry) {
41
+ return this.accessToken;
42
+ }
43
+
44
+ const res = await fetch(GmailClient.TOKEN_URL, {
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
47
+ body: new URLSearchParams({
48
+ client_id: this.config.clientId,
49
+ client_secret: this.config.clientSecret,
50
+ refresh_token: this.config.refreshToken,
51
+ grant_type: 'refresh_token',
52
+ }),
53
+ });
54
+
55
+ if (!res.ok) {
56
+ const text = await res.text();
57
+ throw new Error(`Token refresh failed (${res.status}): ${text}`);
58
+ }
59
+
60
+ const data = (await res.json()) as { access_token: string; expires_in: number };
61
+ this.accessToken = data.access_token;
62
+ this.tokenExpiry = Date.now() + (data.expires_in - 60) * 1000;
63
+ return this.accessToken;
64
+ }
65
+
66
+ private async request(path: string, options: RequestInit = {}): Promise<unknown> {
67
+ const token = await this.getAccessToken();
68
+ const url = `${GmailClient.BASE}${path}`;
69
+
70
+ const res = await fetch(url, {
71
+ ...options,
72
+ headers: {
73
+ Authorization: `Bearer ${token}`,
74
+ 'Content-Type': 'application/json',
75
+ ...options.headers,
76
+ },
77
+ });
78
+
79
+ if (!res.ok) {
80
+ const text = await res.text();
81
+ throw new Error(`Gmail API error (${res.status}): ${text}`);
82
+ }
83
+
84
+ const contentType = res.headers.get('content-type') || '';
85
+ if (contentType.includes('application/json')) {
86
+ return res.json();
87
+ }
88
+ return {};
89
+ }
90
+
91
+ // ========== RFC 2822 Message Builder ==========
92
+
93
+ private buildRawMessage(opts: {
94
+ to: string;
95
+ subject: string;
96
+ body: string;
97
+ cc?: string;
98
+ bcc?: string;
99
+ html?: string;
100
+ in_reply_to?: string;
101
+ references?: string;
102
+ }): string {
103
+ const boundary = `boundary_${Date.now()}_${Math.random().toString(36).slice(2)}`;
104
+ const lines: string[] = [];
105
+
106
+ lines.push(`To: ${opts.to}`);
107
+ if (opts.cc) lines.push(`Cc: ${opts.cc}`);
108
+ if (opts.bcc) lines.push(`Bcc: ${opts.bcc}`);
109
+ lines.push(`Subject: =?UTF-8?B?${this.base64Encode(opts.subject)}?=`);
110
+ if (opts.in_reply_to) lines.push(`In-Reply-To: ${opts.in_reply_to}`);
111
+ if (opts.references) lines.push(`References: ${opts.references}`);
112
+ lines.push('MIME-Version: 1.0');
113
+
114
+ if (opts.html) {
115
+ lines.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
116
+ lines.push('');
117
+ lines.push(`--${boundary}`);
118
+ lines.push('Content-Type: text/plain; charset="UTF-8"');
119
+ lines.push('Content-Transfer-Encoding: base64');
120
+ lines.push('');
121
+ lines.push(this.base64Encode(opts.body));
122
+ lines.push(`--${boundary}`);
123
+ lines.push('Content-Type: text/html; charset="UTF-8"');
124
+ lines.push('Content-Transfer-Encoding: base64');
125
+ lines.push('');
126
+ lines.push(this.base64Encode(opts.html));
127
+ lines.push(`--${boundary}--`);
128
+ } else {
129
+ lines.push('Content-Type: text/plain; charset="UTF-8"');
130
+ lines.push('Content-Transfer-Encoding: base64');
131
+ lines.push('');
132
+ lines.push(this.base64Encode(opts.body));
133
+ }
134
+
135
+ const raw = lines.join('\r\n');
136
+ return this.base64UrlEncode(raw);
137
+ }
138
+
139
+ private base64Encode(str: string): string {
140
+ if (typeof Buffer !== 'undefined') {
141
+ return Buffer.from(str, 'utf-8').toString('base64');
142
+ }
143
+ const encoder = new TextEncoder();
144
+ const bytes = encoder.encode(str);
145
+ let binary = '';
146
+ for (const byte of bytes) {
147
+ binary += String.fromCharCode(byte);
148
+ }
149
+ return btoa(binary);
150
+ }
151
+
152
+ private base64UrlEncode(str: string): string {
153
+ if (typeof Buffer !== 'undefined') {
154
+ return Buffer.from(str, 'utf-8').toString('base64url');
155
+ }
156
+ const encoder = new TextEncoder();
157
+ const bytes = encoder.encode(str);
158
+ let binary = '';
159
+ for (const byte of bytes) {
160
+ binary += String.fromCharCode(byte);
161
+ }
162
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
163
+ }
164
+
165
+ // ========== Messages (10) ==========
166
+
167
+ async listMessages(opts: {
168
+ q?: string;
169
+ labelIds?: string[];
170
+ maxResults?: number;
171
+ pageToken?: string;
172
+ includeSpamTrash?: boolean;
173
+ }): Promise<MessageList> {
174
+ const params = new URLSearchParams();
175
+ if (opts.q) params.set('q', opts.q);
176
+ if (opts.labelIds) {
177
+ for (const id of opts.labelIds) params.append('labelIds', id);
178
+ }
179
+ if (opts.maxResults) params.set('maxResults', String(opts.maxResults));
180
+ if (opts.pageToken) params.set('pageToken', opts.pageToken);
181
+ if (opts.includeSpamTrash) params.set('includeSpamTrash', 'true');
182
+ const qs = params.toString();
183
+ return this.request(`/users/me/messages${qs ? `?${qs}` : ''}`) as Promise<MessageList>;
184
+ }
185
+
186
+ async getMessage(opts: {
187
+ id: string;
188
+ format?: string;
189
+ metadataHeaders?: string[];
190
+ }): Promise<Message> {
191
+ const params = new URLSearchParams();
192
+ if (opts.format) params.set('format', opts.format);
193
+ if (opts.metadataHeaders) {
194
+ for (const h of opts.metadataHeaders) params.append('metadataHeaders', h);
195
+ }
196
+ const qs = params.toString();
197
+ return this.request(`/users/me/messages/${opts.id}${qs ? `?${qs}` : ''}`) as Promise<Message>;
198
+ }
199
+
200
+ async sendMessage(opts: {
201
+ to: string;
202
+ subject: string;
203
+ body: string;
204
+ cc?: string;
205
+ bcc?: string;
206
+ html?: string;
207
+ in_reply_to?: string;
208
+ references?: string;
209
+ thread_id?: string;
210
+ }): Promise<Message> {
211
+ const raw = this.buildRawMessage(opts);
212
+ const payload: Record<string, unknown> = { raw };
213
+ if (opts.thread_id) payload.threadId = opts.thread_id;
214
+ return this.request('/users/me/messages/send', {
215
+ method: 'POST',
216
+ body: JSON.stringify(payload),
217
+ }) as Promise<Message>;
218
+ }
219
+
220
+ async deleteMessage(opts: { id: string }): Promise<void> {
221
+ await this.request(`/users/me/messages/${opts.id}`, {
222
+ method: 'DELETE',
223
+ });
224
+ }
225
+
226
+ async trashMessage(opts: { id: string }): Promise<Message> {
227
+ return this.request(`/users/me/messages/${opts.id}/trash`, {
228
+ method: 'POST',
229
+ }) as Promise<Message>;
230
+ }
231
+
232
+ async untrashMessage(opts: { id: string }): Promise<Message> {
233
+ return this.request(`/users/me/messages/${opts.id}/untrash`, {
234
+ method: 'POST',
235
+ }) as Promise<Message>;
236
+ }
237
+
238
+ async modifyMessage(opts: {
239
+ id: string;
240
+ addLabelIds?: string[];
241
+ removeLabelIds?: string[];
242
+ }): Promise<Message> {
243
+ return this.request(`/users/me/messages/${opts.id}/modify`, {
244
+ method: 'POST',
245
+ body: JSON.stringify({
246
+ addLabelIds: opts.addLabelIds || [],
247
+ removeLabelIds: opts.removeLabelIds || [],
248
+ }),
249
+ }) as Promise<Message>;
250
+ }
251
+
252
+ async batchDeleteMessages(opts: { ids: string[] }): Promise<void> {
253
+ await this.request('/users/me/messages/batchDelete', {
254
+ method: 'POST',
255
+ body: JSON.stringify({ ids: opts.ids }),
256
+ });
257
+ }
258
+
259
+ async batchModifyMessages(opts: {
260
+ ids: string[];
261
+ addLabelIds?: string[];
262
+ removeLabelIds?: string[];
263
+ }): Promise<void> {
264
+ await this.request('/users/me/messages/batchModify', {
265
+ method: 'POST',
266
+ body: JSON.stringify({
267
+ ids: opts.ids,
268
+ addLabelIds: opts.addLabelIds || [],
269
+ removeLabelIds: opts.removeLabelIds || [],
270
+ }),
271
+ });
272
+ }
273
+
274
+ async getAttachment(opts: {
275
+ messageId: string;
276
+ attachmentId: string;
277
+ }): Promise<Attachment> {
278
+ return this.request(
279
+ `/users/me/messages/${opts.messageId}/attachments/${opts.attachmentId}`
280
+ ) as Promise<Attachment>;
281
+ }
282
+
283
+ // ========== Drafts (6) ==========
284
+
285
+ async listDrafts(opts: {
286
+ maxResults?: number;
287
+ pageToken?: string;
288
+ q?: string;
289
+ }): Promise<DraftList> {
290
+ const params = new URLSearchParams();
291
+ if (opts.maxResults) params.set('maxResults', String(opts.maxResults));
292
+ if (opts.pageToken) params.set('pageToken', opts.pageToken);
293
+ if (opts.q) params.set('q', opts.q);
294
+ const qs = params.toString();
295
+ return this.request(`/users/me/drafts${qs ? `?${qs}` : ''}`) as Promise<DraftList>;
296
+ }
297
+
298
+ async getDraft(opts: {
299
+ id: string;
300
+ format?: string;
301
+ }): Promise<Draft> {
302
+ const params = new URLSearchParams();
303
+ if (opts.format) params.set('format', opts.format);
304
+ const qs = params.toString();
305
+ return this.request(`/users/me/drafts/${opts.id}${qs ? `?${qs}` : ''}`) as Promise<Draft>;
306
+ }
307
+
308
+ async createDraft(opts: {
309
+ to: string;
310
+ subject: string;
311
+ body: string;
312
+ cc?: string;
313
+ bcc?: string;
314
+ html?: string;
315
+ thread_id?: string;
316
+ }): Promise<Draft> {
317
+ const raw = this.buildRawMessage(opts);
318
+ const message: Record<string, unknown> = { raw };
319
+ if (opts.thread_id) message.threadId = opts.thread_id;
320
+ return this.request('/users/me/drafts', {
321
+ method: 'POST',
322
+ body: JSON.stringify({ message }),
323
+ }) as Promise<Draft>;
324
+ }
325
+
326
+ async updateDraft(opts: {
327
+ id: string;
328
+ to: string;
329
+ subject: string;
330
+ body: string;
331
+ cc?: string;
332
+ bcc?: string;
333
+ html?: string;
334
+ thread_id?: string;
335
+ }): Promise<Draft> {
336
+ const raw = this.buildRawMessage(opts);
337
+ const message: Record<string, unknown> = { raw };
338
+ if (opts.thread_id) message.threadId = opts.thread_id;
339
+ return this.request(`/users/me/drafts/${opts.id}`, {
340
+ method: 'PUT',
341
+ body: JSON.stringify({ message }),
342
+ }) as Promise<Draft>;
343
+ }
344
+
345
+ async deleteDraft(opts: { id: string }): Promise<void> {
346
+ await this.request(`/users/me/drafts/${opts.id}`, {
347
+ method: 'DELETE',
348
+ });
349
+ }
350
+
351
+ async sendDraft(opts: { id: string }): Promise<Message> {
352
+ return this.request('/users/me/drafts/send', {
353
+ method: 'POST',
354
+ body: JSON.stringify({ id: opts.id }),
355
+ }) as Promise<Message>;
356
+ }
357
+
358
+ // ========== Labels (5) ==========
359
+
360
+ async listLabels(): Promise<LabelList> {
361
+ return this.request('/users/me/labels') as Promise<LabelList>;
362
+ }
363
+
364
+ async getLabel(opts: { id: string }): Promise<Label> {
365
+ return this.request(`/users/me/labels/${opts.id}`) as Promise<Label>;
366
+ }
367
+
368
+ async createLabel(opts: {
369
+ name: string;
370
+ messageListVisibility?: string;
371
+ labelListVisibility?: string;
372
+ backgroundColor?: string;
373
+ textColor?: string;
374
+ }): Promise<Label> {
375
+ const payload: Record<string, unknown> = { name: opts.name };
376
+ if (opts.messageListVisibility) payload.messageListVisibility = opts.messageListVisibility;
377
+ if (opts.labelListVisibility) payload.labelListVisibility = opts.labelListVisibility;
378
+ if (opts.backgroundColor || opts.textColor) {
379
+ payload.color = {
380
+ backgroundColor: opts.backgroundColor,
381
+ textColor: opts.textColor,
382
+ };
383
+ }
384
+ return this.request('/users/me/labels', {
385
+ method: 'POST',
386
+ body: JSON.stringify(payload),
387
+ }) as Promise<Label>;
388
+ }
389
+
390
+ async updateLabel(opts: {
391
+ id: string;
392
+ name?: string;
393
+ messageListVisibility?: string;
394
+ labelListVisibility?: string;
395
+ backgroundColor?: string;
396
+ textColor?: string;
397
+ }): Promise<Label> {
398
+ const payload: Record<string, unknown> = { id: opts.id };
399
+ if (opts.name) payload.name = opts.name;
400
+ if (opts.messageListVisibility) payload.messageListVisibility = opts.messageListVisibility;
401
+ if (opts.labelListVisibility) payload.labelListVisibility = opts.labelListVisibility;
402
+ if (opts.backgroundColor || opts.textColor) {
403
+ payload.color = {
404
+ backgroundColor: opts.backgroundColor,
405
+ textColor: opts.textColor,
406
+ };
407
+ }
408
+ return this.request(`/users/me/labels/${opts.id}`, {
409
+ method: 'PUT',
410
+ body: JSON.stringify(payload),
411
+ }) as Promise<Label>;
412
+ }
413
+
414
+ async deleteLabel(opts: { id: string }): Promise<void> {
415
+ await this.request(`/users/me/labels/${opts.id}`, {
416
+ method: 'DELETE',
417
+ });
418
+ }
419
+
420
+ // ========== Threads (5) ==========
421
+
422
+ async listThreads(opts: {
423
+ q?: string;
424
+ labelIds?: string[];
425
+ maxResults?: number;
426
+ pageToken?: string;
427
+ includeSpamTrash?: boolean;
428
+ }): Promise<ThreadList> {
429
+ const params = new URLSearchParams();
430
+ if (opts.q) params.set('q', opts.q);
431
+ if (opts.labelIds) {
432
+ for (const id of opts.labelIds) params.append('labelIds', id);
433
+ }
434
+ if (opts.maxResults) params.set('maxResults', String(opts.maxResults));
435
+ if (opts.pageToken) params.set('pageToken', opts.pageToken);
436
+ if (opts.includeSpamTrash) params.set('includeSpamTrash', 'true');
437
+ const qs = params.toString();
438
+ return this.request(`/users/me/threads${qs ? `?${qs}` : ''}`) as Promise<ThreadList>;
439
+ }
440
+
441
+ async getThread(opts: {
442
+ id: string;
443
+ format?: string;
444
+ }): Promise<Thread> {
445
+ const params = new URLSearchParams();
446
+ if (opts.format) params.set('format', opts.format);
447
+ const qs = params.toString();
448
+ return this.request(`/users/me/threads/${opts.id}${qs ? `?${qs}` : ''}`) as Promise<Thread>;
449
+ }
450
+
451
+ async modifyThread(opts: {
452
+ id: string;
453
+ addLabelIds?: string[];
454
+ removeLabelIds?: string[];
455
+ }): Promise<Thread> {
456
+ return this.request(`/users/me/threads/${opts.id}/modify`, {
457
+ method: 'POST',
458
+ body: JSON.stringify({
459
+ addLabelIds: opts.addLabelIds || [],
460
+ removeLabelIds: opts.removeLabelIds || [],
461
+ }),
462
+ }) as Promise<Thread>;
463
+ }
464
+
465
+ async trashThread(opts: { id: string }): Promise<Thread> {
466
+ return this.request(`/users/me/threads/${opts.id}/trash`, {
467
+ method: 'POST',
468
+ }) as Promise<Thread>;
469
+ }
470
+
471
+ async untrashThread(opts: { id: string }): Promise<Thread> {
472
+ return this.request(`/users/me/threads/${opts.id}/untrash`, {
473
+ method: 'POST',
474
+ }) as Promise<Thread>;
475
+ }
476
+
477
+ // ========== Settings (2) ==========
478
+
479
+ async getProfile(): Promise<Profile> {
480
+ return this.request('/users/me/profile') as Promise<Profile>;
481
+ }
482
+
483
+ async updateVacation(opts: {
484
+ enableAutoReply: boolean;
485
+ responseSubject?: string;
486
+ responseBodyPlainText?: string;
487
+ responseBodyHtml?: string;
488
+ restrictToContacts?: boolean;
489
+ restrictToDomain?: boolean;
490
+ startTime?: string;
491
+ endTime?: string;
492
+ }): Promise<VacationSettings> {
493
+ return this.request('/users/me/settings/vacation', {
494
+ method: 'PUT',
495
+ body: JSON.stringify({
496
+ enableAutoReply: opts.enableAutoReply,
497
+ responseSubject: opts.responseSubject,
498
+ responseBodyPlainText: opts.responseBodyPlainText,
499
+ responseBodyHtml: opts.responseBodyHtml,
500
+ restrictToContacts: opts.restrictToContacts,
501
+ restrictToDomain: opts.restrictToDomain,
502
+ startTime: opts.startTime,
503
+ endTime: opts.endTime,
504
+ }),
505
+ }) as Promise<VacationSettings>;
506
+ }
507
+ }
package/src/index.ts ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Gmail MCP Server
4
+ *
5
+ * Community edition — connects directly to Gmail API v1.
6
+ *
7
+ * Usage (stdio - for Claude Desktop / Cursor / VS Code):
8
+ * GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=xxx GOOGLE_REFRESH_TOKEN=xxx npx @node2flow/gmail-mcp
9
+ *
10
+ * Usage (HTTP - Streamable HTTP transport):
11
+ * GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=xxx GOOGLE_REFRESH_TOKEN=xxx npx @node2flow/gmail-mcp --http
12
+ */
13
+
14
+ import { randomUUID } from 'node:crypto';
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import {
17
+ StreamableHTTPServerTransport,
18
+ } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
19
+ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
20
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
21
+
22
+ import { createServer } from './server.js';
23
+ import { TOOLS } from './tools.js';
24
+
25
+ function getConfig() {
26
+ const clientId = process.env.GOOGLE_CLIENT_ID;
27
+ const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
28
+ const refreshToken = process.env.GOOGLE_REFRESH_TOKEN;
29
+ if (!clientId || !clientSecret || !refreshToken) return null;
30
+ return { clientId, clientSecret, refreshToken };
31
+ }
32
+
33
+ async function startStdio() {
34
+ const config = getConfig();
35
+ const server = createServer(config ?? undefined);
36
+ const transport = new StdioServerTransport();
37
+ await server.connect(transport);
38
+
39
+ console.error('Gmail MCP Server running on stdio');
40
+ console.error(`OAuth: ${config ? '***configured***' : '(not configured — set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN)'}`);
41
+ console.error(`Tools available: ${TOOLS.length}`);
42
+ console.error('Ready for MCP client\n');
43
+ }
44
+
45
+ async function startHttp() {
46
+ const port = parseInt(process.env.PORT || '3000', 10);
47
+ const app = createMcpExpressApp({ host: '0.0.0.0' });
48
+
49
+ const transports: Record<string, StreamableHTTPServerTransport> = {};
50
+
51
+ app.post('/mcp', async (req: any, res: any) => {
52
+ const url = new URL(req.url, `http://${req.headers.host}`);
53
+ const qClientId = url.searchParams.get('GOOGLE_CLIENT_ID');
54
+ const qClientSecret = url.searchParams.get('GOOGLE_CLIENT_SECRET');
55
+ const qRefreshToken = url.searchParams.get('GOOGLE_REFRESH_TOKEN');
56
+ if (qClientId) process.env.GOOGLE_CLIENT_ID = qClientId;
57
+ if (qClientSecret) process.env.GOOGLE_CLIENT_SECRET = qClientSecret;
58
+ if (qRefreshToken) process.env.GOOGLE_REFRESH_TOKEN = qRefreshToken;
59
+
60
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
61
+
62
+ try {
63
+ let transport: StreamableHTTPServerTransport;
64
+
65
+ if (sessionId && transports[sessionId]) {
66
+ transport = transports[sessionId];
67
+ } else if (!sessionId && isInitializeRequest(req.body)) {
68
+ transport = new StreamableHTTPServerTransport({
69
+ sessionIdGenerator: () => randomUUID(),
70
+ onsessioninitialized: (sid: string) => {
71
+ transports[sid] = transport;
72
+ },
73
+ });
74
+
75
+ transport.onclose = () => {
76
+ const sid = transport.sessionId;
77
+ if (sid && transports[sid]) {
78
+ delete transports[sid];
79
+ }
80
+ };
81
+
82
+ const config = getConfig();
83
+ const server = createServer(config ?? undefined);
84
+ await server.connect(transport);
85
+ await transport.handleRequest(req, res, req.body);
86
+ return;
87
+ } else {
88
+ res.status(400).json({
89
+ jsonrpc: '2.0',
90
+ error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
91
+ id: null,
92
+ });
93
+ return;
94
+ }
95
+
96
+ await transport.handleRequest(req, res, req.body);
97
+ } catch (error) {
98
+ console.error('Error handling MCP request:', error);
99
+ if (!res.headersSent) {
100
+ res.status(500).json({
101
+ jsonrpc: '2.0',
102
+ error: { code: -32603, message: 'Internal server error' },
103
+ id: null,
104
+ });
105
+ }
106
+ }
107
+ });
108
+
109
+ app.get('/mcp', async (req: any, res: any) => {
110
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
111
+ if (!sessionId || !transports[sessionId]) {
112
+ res.status(400).send('Invalid or missing session ID');
113
+ return;
114
+ }
115
+ await transports[sessionId].handleRequest(req, res);
116
+ });
117
+
118
+ app.delete('/mcp', async (req: any, res: any) => {
119
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
120
+ if (!sessionId || !transports[sessionId]) {
121
+ res.status(400).send('Invalid or missing session ID');
122
+ return;
123
+ }
124
+ await transports[sessionId].handleRequest(req, res);
125
+ });
126
+
127
+ app.get('/', (_req: any, res: any) => {
128
+ res.json({
129
+ name: 'gmail-mcp',
130
+ version: '1.0.0',
131
+ status: 'ok',
132
+ tools: TOOLS.length,
133
+ transport: 'streamable-http',
134
+ endpoints: { mcp: '/mcp' },
135
+ });
136
+ });
137
+
138
+ const config = getConfig();
139
+ app.listen(port, () => {
140
+ console.log(`Gmail MCP Server (HTTP) listening on port ${port}`);
141
+ console.log(`OAuth: ${config ? '***configured***' : '(not configured — set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN)'}`);
142
+ console.log(`Tools available: ${TOOLS.length}`);
143
+ console.log(`MCP endpoint: http://localhost:${port}/mcp`);
144
+ });
145
+
146
+ process.on('SIGINT', async () => {
147
+ console.log('\nShutting down...');
148
+ for (const sessionId in transports) {
149
+ try {
150
+ await transports[sessionId].close();
151
+ delete transports[sessionId];
152
+ } catch { /* ignore */ }
153
+ }
154
+ process.exit(0);
155
+ });
156
+ }
157
+
158
+ async function main() {
159
+ const useHttp = process.argv.includes('--http');
160
+ if (useHttp) {
161
+ await startHttp();
162
+ } else {
163
+ await startStdio();
164
+ }
165
+ }
166
+
167
+ export default function createSmitheryServer(opts?: {
168
+ config?: {
169
+ GOOGLE_CLIENT_ID?: string;
170
+ GOOGLE_CLIENT_SECRET?: string;
171
+ GOOGLE_REFRESH_TOKEN?: string;
172
+ };
173
+ }) {
174
+ if (opts?.config?.GOOGLE_CLIENT_ID) process.env.GOOGLE_CLIENT_ID = opts.config.GOOGLE_CLIENT_ID;
175
+ if (opts?.config?.GOOGLE_CLIENT_SECRET) process.env.GOOGLE_CLIENT_SECRET = opts.config.GOOGLE_CLIENT_SECRET;
176
+ if (opts?.config?.GOOGLE_REFRESH_TOKEN) process.env.GOOGLE_REFRESH_TOKEN = opts.config.GOOGLE_REFRESH_TOKEN;
177
+ const config = getConfig();
178
+ return createServer(config ?? undefined);
179
+ }
180
+
181
+ main().catch((error) => {
182
+ console.error('Fatal error:', error);
183
+ process.exit(1);
184
+ });