google-tools-mcp 1.2.12 → 2.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.
- package/LICENSE +21 -21
- package/README.md +537 -276
- package/dist/cachedToolsList.js +52 -52
- package/dist/googleDocsApiHelpers.js +76 -12
- package/dist/helpers.js +572 -252
- package/dist/httpAuth.js +296 -0
- package/dist/index.js +162 -11
- package/dist/logger.js +87 -87
- package/dist/markdown-transformer/docsToMarkdown.js +128 -0
- package/dist/markdown-transformer/index.js +17 -7
- package/dist/markdown-transformer/markdownToDocs.js +185 -27
- package/dist/readTracker.js +136 -17
- package/dist/sessionContext.js +37 -0
- package/dist/setup.js +187 -9
- package/dist/tools/calendar/getBusy.js +64 -64
- package/dist/tools/calendar/getEvents.js +141 -141
- package/dist/tools/calendar/getFree.js +225 -225
- package/dist/tools/calendar/index.js +19 -19
- package/dist/tools/calendar/listCalendars.js +38 -38
- package/dist/tools/calendar/listRecurringInstances.js +83 -83
- package/dist/tools/calendar/manageCalendar.js +121 -121
- package/dist/tools/calendar/manageEvent.js +258 -258
- package/dist/tools/calendar/moveEvent.js +66 -66
- package/dist/tools/docs/addTab.js +10 -12
- package/dist/tools/docs/appendToGoogleDoc.js +9 -4
- package/dist/tools/docs/deleteRange.js +9 -4
- package/dist/tools/docs/findAndReplace.js +9 -4
- package/dist/tools/docs/formatting/applyParagraphStyle.js +4 -1
- package/dist/tools/docs/formatting/getFormatting.js +171 -171
- package/dist/tools/docs/insertImage.js +15 -2
- package/dist/tools/docs/insertPageBreak.js +4 -1
- package/dist/tools/docs/insertTable.js +4 -1
- package/dist/tools/docs/insertTableWithData.js +4 -1
- package/dist/tools/docs/modifyText.js +14 -4
- package/dist/tools/docs/modifyText.test.js +84 -84
- package/dist/tools/docs/readGoogleDoc.js +71 -11
- package/dist/tools/docs/renameTab.js +12 -14
- package/dist/tools/drafts.js +165 -165
- package/dist/tools/drive/createDocument.js +12 -0
- package/dist/tools/drive/downloadFile.js +268 -268
- package/dist/tools/drive/getFileInfo.js +48 -48
- package/dist/tools/drive/getFilePath.js +50 -50
- package/dist/tools/drive/listDriveFiles.js +112 -112
- package/dist/tools/drive/listSharedDrives.js +52 -52
- package/dist/tools/drive/listSharedWithMe.js +97 -97
- package/dist/tools/drive/uploadFile.js +111 -111
- package/dist/tools/extras/index.js +9 -9
- package/dist/tools/extras/readDriveFile.js +197 -196
- package/dist/tools/extras/readFile.js +84 -84
- package/dist/tools/extras/searchFileContents.js +81 -81
- package/dist/tools/forms/batchUpdateForm.js +81 -81
- package/dist/tools/forms/createForm.js +54 -54
- package/dist/tools/forms/getForm.js +118 -118
- package/dist/tools/forms/getFormResponse.js +45 -45
- package/dist/tools/forms/index.js +15 -15
- package/dist/tools/forms/listFormResponses.js +56 -56
- package/dist/tools/forms/setPublishSettings.js +59 -59
- package/dist/tools/gmail/drafts.js +165 -165
- package/dist/tools/gmail/labels.js +54 -83
- package/dist/tools/gmail/messages.js +444 -441
- package/dist/tools/gmail/settings.js +358 -528
- package/dist/tools/gmail/threads.js +286 -164
- package/dist/tools/index.js +511 -473
- package/dist/tools/labels.js +103 -103
- package/dist/tools/legacyAliases.js +426 -0
- package/dist/tools/maps/directions.js +72 -0
- package/dist/tools/maps/geocode.js +16 -0
- package/dist/tools/maps/index.js +15 -0
- package/dist/tools/maps/mapsClient.js +83 -0
- package/dist/tools/maps/placeDetails.js +16 -0
- package/dist/tools/maps/reverseGeocode.js +19 -0
- package/dist/tools/maps/searchNearby.js +54 -0
- package/dist/tools/maps/searchPlaces.js +24 -0
- package/dist/tools/messages.js +448 -448
- package/dist/tools/settings.js +528 -528
- package/dist/tools/slides/createPresentation.js +104 -104
- package/dist/tools/slides/createShape.js +92 -92
- package/dist/tools/slides/createTextBox.js +82 -82
- package/dist/tools/slides/deleteSlide.js +30 -30
- package/dist/tools/slides/duplicateSlide.js +37 -37
- package/dist/tools/slides/exportThumbnail.js +42 -42
- package/dist/tools/slides/formatParagraph.js +72 -72
- package/dist/tools/slides/formatText.js +84 -84
- package/dist/tools/slides/getPresentation.js +87 -87
- package/dist/tools/slides/index.js +33 -33
- package/dist/tools/slides/reorderSlides.js +41 -41
- package/dist/tools/slides/replaceAllText.js +46 -46
- package/dist/tools/slides/setBackground.js +58 -58
- package/dist/tools/slides/speakerNotes.js +102 -102
- package/dist/tools/slides/styleShape.js +111 -111
- package/dist/tools/slides/updatePresentation.js +128 -128
- package/dist/tools/threads.js +145 -145
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +30 -6
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +71 -8
- package/dist/updateCheck.js +202 -0
- package/dist/workspace.js +122 -0
- package/package.json +82 -81
package/dist/tools/index.js
CHANGED
|
@@ -1,473 +1,511 @@
|
|
|
1
|
-
// Tool registration — all categories loaded eagerly at startup.
|
|
2
|
-
// (Claude Code doesn't support notifications/tools/list_changed,
|
|
3
|
-
// so lazy-loading doesn't work.)
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
import * as fs from 'fs/promises';
|
|
6
|
-
import * as path from 'path';
|
|
7
|
-
import { fileURLToPath } from 'url';
|
|
8
|
-
import * as os from 'os';
|
|
9
|
-
import { exec } from 'child_process';
|
|
10
|
-
import { promisify } from 'util';
|
|
11
|
-
import { getTokenPath, getConfigDir, SCOPES } from '../auth.js';
|
|
12
|
-
import { resetClients, withAuthRetry, getAuthClientIfReady } from '../clients.js';
|
|
13
|
-
import { logger } from '../logger.js';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return { ok:
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
cmd = `
|
|
57
|
-
} else {
|
|
58
|
-
cmd = `
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
'
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
register(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
report.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
report.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
report.
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
let
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
await fs.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
1
|
+
// Tool registration — all categories loaded eagerly at startup.
|
|
2
|
+
// (Claude Code doesn't support notifications/tools/list_changed,
|
|
3
|
+
// so lazy-loading doesn't work.)
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import * as fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import { exec } from 'child_process';
|
|
10
|
+
import { promisify } from 'util';
|
|
11
|
+
import { getTokenPath, getConfigDir, SCOPES } from '../auth.js';
|
|
12
|
+
import { resetClients, withAuthRetry, getAuthClientIfReady } from '../clients.js';
|
|
13
|
+
import { logger } from '../logger.js';
|
|
14
|
+
import { runWithSession } from '../sessionContext.js';
|
|
15
|
+
import { google } from 'googleapis';
|
|
16
|
+
import { registerLegacyAliases } from './legacyAliases.js';
|
|
17
|
+
|
|
18
|
+
const execAsync = promisify(exec);
|
|
19
|
+
|
|
20
|
+
const REPO = 'karthikcsq/google-tools-mcp';
|
|
21
|
+
|
|
22
|
+
async function tryGhCli(title, body, label) {
|
|
23
|
+
// Probe for gh CLI
|
|
24
|
+
try {
|
|
25
|
+
await execAsync('gh --version');
|
|
26
|
+
} catch {
|
|
27
|
+
return { ok: false, reason: 'gh CLI not installed' };
|
|
28
|
+
}
|
|
29
|
+
// Probe for auth
|
|
30
|
+
try {
|
|
31
|
+
await execAsync('gh auth status');
|
|
32
|
+
} catch {
|
|
33
|
+
return { ok: false, reason: 'gh CLI not authenticated (run: gh auth login)' };
|
|
34
|
+
}
|
|
35
|
+
// Write body to a temp file to avoid shell-escaping issues with newlines/quotes.
|
|
36
|
+
const tmpFile = path.join(os.tmpdir(), `gtm-feedback-${Date.now()}-${Math.random().toString(36).slice(2)}.md`);
|
|
37
|
+
try {
|
|
38
|
+
await fs.writeFile(tmpFile, body, 'utf8');
|
|
39
|
+
const { stdout } = await execAsync(
|
|
40
|
+
`gh issue create --repo ${REPO} --title ${JSON.stringify(title)} --label ${JSON.stringify(label)} --body-file ${JSON.stringify(tmpFile)}`,
|
|
41
|
+
{ maxBuffer: 10 * 1024 * 1024 }
|
|
42
|
+
);
|
|
43
|
+
const issueUrl = stdout.trim().split('\n').pop();
|
|
44
|
+
return { ok: true, issueUrl };
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return { ok: false, reason: `gh CLI failed: ${err.stderr || err.message || err}` };
|
|
47
|
+
} finally {
|
|
48
|
+
try { await fs.unlink(tmpFile); } catch {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function openBrowser(url) {
|
|
53
|
+
const platform = process.platform;
|
|
54
|
+
let cmd;
|
|
55
|
+
if (platform === 'win32') {
|
|
56
|
+
cmd = `start "" "${url}"`;
|
|
57
|
+
} else if (platform === 'darwin') {
|
|
58
|
+
cmd = `open "${url}"`;
|
|
59
|
+
} else {
|
|
60
|
+
cmd = `xdg-open "${url}"`;
|
|
61
|
+
}
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
exec(cmd, (err) => resolve(!err));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Wrap server.addTool so every tool's execute() auto-retries on invalid_grant
|
|
69
|
+
// and appends a troubleshoot/feedback hint to errors.
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
const ERROR_HINT =
|
|
72
|
+
'\n\nIf this error is unexpected or unclear, you can:\n' +
|
|
73
|
+
' • Call the `troubleshoot` tool to run a health check (auth, API connectivity, recent logs).\n' +
|
|
74
|
+
' • Call the `feedback` tool to file a bug report with diagnostics auto-attached.';
|
|
75
|
+
|
|
76
|
+
// Tools that should NOT have the hint appended (would be circular/noisy).
|
|
77
|
+
const HINT_EXCLUDED_TOOLS = new Set(['troubleshoot', 'feedback', 'help', 'logout']);
|
|
78
|
+
|
|
79
|
+
function appendHintToError(error, toolName) {
|
|
80
|
+
if (HINT_EXCLUDED_TOOLS.has(toolName)) return error;
|
|
81
|
+
if (!error) return error;
|
|
82
|
+
// Avoid double-appending if something else (or a retry) already added it.
|
|
83
|
+
const existingMsg = error.message || '';
|
|
84
|
+
if (existingMsg.includes('`troubleshoot` tool')) return error;
|
|
85
|
+
try {
|
|
86
|
+
error.message = existingMsg + ERROR_HINT;
|
|
87
|
+
} catch {
|
|
88
|
+
// Some error types have non-writable message; fall back to a new error.
|
|
89
|
+
const wrapped = new Error(existingMsg + ERROR_HINT);
|
|
90
|
+
wrapped.cause = error;
|
|
91
|
+
return wrapped;
|
|
92
|
+
}
|
|
93
|
+
return error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function wrapServerWithAuthRetry(server) {
|
|
97
|
+
const originalAddTool = server.addTool.bind(server);
|
|
98
|
+
server.addTool = function (toolDef) {
|
|
99
|
+
const originalExecute = toolDef.execute;
|
|
100
|
+
const toolName = toolDef.name;
|
|
101
|
+
if (originalExecute) {
|
|
102
|
+
toolDef.execute = async function (...args) {
|
|
103
|
+
// Bind this request's MCP session so per-session state (e.g. the
|
|
104
|
+
// read-before-edit tracker) is isolated across concurrent HTTP
|
|
105
|
+
// clients. context.sessionId is undefined for stdio (single
|
|
106
|
+
// client), which maps to the default namespace. (PR #36 review)
|
|
107
|
+
const sessionKey = args[1]?.sessionId ?? null;
|
|
108
|
+
return runWithSession(sessionKey, async () => {
|
|
109
|
+
try {
|
|
110
|
+
return await withAuthRetry(() => originalExecute.apply(this, args));
|
|
111
|
+
} catch (err) {
|
|
112
|
+
throw appendHintToError(err, toolName);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return originalAddTool(toolDef);
|
|
118
|
+
};
|
|
119
|
+
return server;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- Category registry ---
|
|
123
|
+
const CATEGORIES = {
|
|
124
|
+
files: {
|
|
125
|
+
async loader(server) {
|
|
126
|
+
const { registerDriveTools } = await import('./drive/index.js');
|
|
127
|
+
const { registerExtrasTools } = await import('./extras/index.js');
|
|
128
|
+
registerDriveTools(server);
|
|
129
|
+
registerExtrasTools(server);
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
documents: {
|
|
133
|
+
async loader(server) {
|
|
134
|
+
const { registerDocsTools } = await import('./docs/index.js');
|
|
135
|
+
const { registerUtilsTools } = await import('./utils/index.js');
|
|
136
|
+
registerDocsTools(server);
|
|
137
|
+
registerUtilsTools(server);
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
spreadsheets: {
|
|
141
|
+
async loader(server) {
|
|
142
|
+
const { registerSheetsTools } = await import('./sheets/index.js');
|
|
143
|
+
registerSheetsTools(server);
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
email: {
|
|
147
|
+
async loader(server) {
|
|
148
|
+
const { register: registerMessages } = await import('./gmail/messages.js');
|
|
149
|
+
const { register: registerDrafts } = await import('./gmail/drafts.js');
|
|
150
|
+
registerMessages(server);
|
|
151
|
+
registerDrafts(server);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
email_threads: {
|
|
155
|
+
async loader(server) {
|
|
156
|
+
const { register } = await import('./gmail/threads.js');
|
|
157
|
+
register(server);
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
email_labels: {
|
|
161
|
+
async loader(server) {
|
|
162
|
+
const { register } = await import('./gmail/labels.js');
|
|
163
|
+
register(server);
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
email_settings: {
|
|
167
|
+
async loader(server) {
|
|
168
|
+
const { register } = await import('./gmail/settings.js');
|
|
169
|
+
register(server);
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
calendar: {
|
|
173
|
+
async loader(server) {
|
|
174
|
+
const { registerCalendarTools } = await import('./calendar/index.js');
|
|
175
|
+
registerCalendarTools(server);
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
forms: {
|
|
179
|
+
async loader(server) {
|
|
180
|
+
const { registerFormsTools } = await import('./forms/index.js');
|
|
181
|
+
registerFormsTools(server);
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
slides: {
|
|
185
|
+
async loader(server) {
|
|
186
|
+
const { registerSlidesTools } = await import('./slides/index.js');
|
|
187
|
+
registerSlidesTools(server);
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
tasks: {
|
|
191
|
+
async loader(server) {
|
|
192
|
+
const { registerTasksTools } = await import('./tasks/index.js');
|
|
193
|
+
registerTasksTools(server);
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
maps: {
|
|
197
|
+
async loader(server) {
|
|
198
|
+
const { registerMapsTools } = await import('./maps/index.js');
|
|
199
|
+
registerMapsTools(server);
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Public: register all tools eagerly, plus the logout utility.
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
export async function registerAllTools(server) {
|
|
208
|
+
// Wrap server so every tool auto-retries on invalid_grant (expired refresh token)
|
|
209
|
+
const wrappedServer = wrapServerWithAuthRetry(server);
|
|
210
|
+
|
|
211
|
+
// Capture each registered tool so the legacy alias layer can look up the
|
|
212
|
+
// new tools' implementations to forward to. We snapshot a shallow copy of
|
|
213
|
+
// toolDef *before* handing it to wrappedAddTool: wrapServerWithAuthRetry
|
|
214
|
+
// mutates toolDef.execute in place to add the auth-retry wrapper, and since
|
|
215
|
+
// registeredTools would otherwise hold a reference to that same (mutated)
|
|
216
|
+
// object, aliases would forward to an already-retry-wrapped execute. Then,
|
|
217
|
+
// because each alias is itself registered through this same addTool (which
|
|
218
|
+
// gets retry-wrapped again), a persistent invalid_grant would invoke the
|
|
219
|
+
// real handler up to 4x and reauthorize repeatedly instead of the
|
|
220
|
+
// documented single retry. The snapshot keeps the map pointing at the raw,
|
|
221
|
+
// unwrapped implementation so the outer (alias-level) wrapper is the only
|
|
222
|
+
// retry layer applied.
|
|
223
|
+
const registeredTools = new Map();
|
|
224
|
+
const wrappedAddTool = wrappedServer.addTool.bind(wrappedServer);
|
|
225
|
+
wrappedServer.addTool = function (toolDef) {
|
|
226
|
+
registeredTools.set(toolDef.name, { ...toolDef });
|
|
227
|
+
return wrappedAddTool(toolDef);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Load every category
|
|
231
|
+
for (const [name, { loader }] of Object.entries(CATEGORIES)) {
|
|
232
|
+
await loader(wrappedServer);
|
|
233
|
+
}
|
|
234
|
+
logger.info(`Loaded all ${Object.keys(CATEGORIES).length} categories at startup.`);
|
|
235
|
+
|
|
236
|
+
// Register backward-compatible snake_case aliases for the renamed/consolidated
|
|
237
|
+
// tools. Opt-in: set GOOGLE_MCP_ENABLE_LEGACY_ALIASES=true to register them.
|
|
238
|
+
registerLegacyAliases(wrappedServer, registeredTools);
|
|
239
|
+
|
|
240
|
+
// --- Help tool (always available) ---
|
|
241
|
+
server.addTool({
|
|
242
|
+
name: 'help',
|
|
243
|
+
description:
|
|
244
|
+
'Show documentation for google-tools-mcp: setup instructions, available tool categories, environment variables, and troubleshooting. ' +
|
|
245
|
+
'Call this when you need guidance on how to use the Google Workspace tools. ' +
|
|
246
|
+
'Also available: `troubleshoot` (run health check when tools fail) and `feedback` (submit bug reports/feature requests with diagnostics auto-attached).',
|
|
247
|
+
parameters: z.object({}),
|
|
248
|
+
execute: async () => {
|
|
249
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
250
|
+
const readmePath = path.resolve(__dirname, '..', '..', 'README.md');
|
|
251
|
+
const diagnosticsSection = '\n\n## Diagnostics & Feedback\n\n' +
|
|
252
|
+
'- **`troubleshoot`** — Run a health check when tools fail (checks auth, API connectivity, config, recent logs).\n' +
|
|
253
|
+
'- **`feedback`** — Submit a bug report or feature request with diagnostics auto-attached (files via GitHub CLI or browser).\n' +
|
|
254
|
+
'- **`help`** — Show this documentation.\n';
|
|
255
|
+
try {
|
|
256
|
+
const readme = await fs.readFile(readmePath, 'utf-8');
|
|
257
|
+
return readme + diagnosticsSection;
|
|
258
|
+
} catch {
|
|
259
|
+
return 'README not found. Visit https://www.npmjs.com/package/google-tools-mcp for documentation.' + diagnosticsSection;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// --- Logout tool (always available) ---
|
|
265
|
+
server.addTool({
|
|
266
|
+
name: 'logout',
|
|
267
|
+
description:
|
|
268
|
+
'Log out of the current Google account by deleting the saved OAuth token. The next tool call will require re-authentication.',
|
|
269
|
+
parameters: z.object({}),
|
|
270
|
+
execute: async () => {
|
|
271
|
+
const tokenPath = getTokenPath();
|
|
272
|
+
try {
|
|
273
|
+
await fs.unlink(tokenPath);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
if (err.code !== 'ENOENT') throw err;
|
|
276
|
+
}
|
|
277
|
+
resetClients();
|
|
278
|
+
return JSON.stringify({
|
|
279
|
+
success: true,
|
|
280
|
+
message: 'Logged out. The next tool call will require re-authentication.',
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// --- Troubleshoot tool (always available) ---
|
|
286
|
+
server.addTool({
|
|
287
|
+
name: 'troubleshoot',
|
|
288
|
+
description:
|
|
289
|
+
'Run a health check on google-tools-mcp: verify OAuth token, test API connectivity, show config and recent logs. Call this when tools are failing or behaving unexpectedly.',
|
|
290
|
+
parameters: z.object({}),
|
|
291
|
+
execute: async () => {
|
|
292
|
+
const report = { auth: {}, services: {}, config: {}, recentLogs: null, environment: {} };
|
|
293
|
+
|
|
294
|
+
// --- Auth status ---
|
|
295
|
+
const tokenPath = getTokenPath();
|
|
296
|
+
try {
|
|
297
|
+
const tokenContent = await fs.readFile(tokenPath, 'utf8');
|
|
298
|
+
const token = JSON.parse(tokenContent);
|
|
299
|
+
report.auth.tokenFile = 'present';
|
|
300
|
+
report.auth.hasRefreshToken = !!token.refresh_token;
|
|
301
|
+
report.auth.type = token.type || 'unknown';
|
|
302
|
+
} catch (err) {
|
|
303
|
+
report.auth.tokenFile = err.code === 'ENOENT' ? 'missing' : 'unreadable';
|
|
304
|
+
report.auth.hasRefreshToken = false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Try refreshing token
|
|
308
|
+
const client = getAuthClientIfReady();
|
|
309
|
+
if (client) {
|
|
310
|
+
try {
|
|
311
|
+
const { credentials } = await client.refreshAccessToken();
|
|
312
|
+
client.setCredentials(credentials);
|
|
313
|
+
report.auth.status = 'valid';
|
|
314
|
+
report.auth.expiry = credentials.expiry_date
|
|
315
|
+
? new Date(credentials.expiry_date).toISOString()
|
|
316
|
+
: 'unknown';
|
|
317
|
+
} catch (err) {
|
|
318
|
+
report.auth.status = 'expired_or_revoked';
|
|
319
|
+
report.auth.refreshError = err.message;
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
report.auth.status = report.auth.tokenFile === 'present' ? 'not_initialized' : 'missing';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// --- Service probes ---
|
|
326
|
+
if (client && report.auth.status === 'valid') {
|
|
327
|
+
// Drive
|
|
328
|
+
try {
|
|
329
|
+
const drive = google.drive({ version: 'v3', auth: client });
|
|
330
|
+
const res = await drive.about.get({ fields: 'user' });
|
|
331
|
+
report.services.drive = { status: 'ok', account: res.data.user?.emailAddress || 'unknown' };
|
|
332
|
+
} catch (err) {
|
|
333
|
+
report.services.drive = { status: 'error', error: err.message };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Gmail
|
|
337
|
+
try {
|
|
338
|
+
const gmail = google.gmail({ version: 'v1', auth: client });
|
|
339
|
+
const res = await gmail.users.getProfile({ userId: 'me' });
|
|
340
|
+
report.services.gmail = { status: 'ok', email: res.data.emailAddress || 'unknown' };
|
|
341
|
+
} catch (err) {
|
|
342
|
+
report.services.gmail = { status: 'error', error: err.message };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Calendar
|
|
346
|
+
try {
|
|
347
|
+
const calendar = google.calendar({ version: 'v3', auth: client });
|
|
348
|
+
await calendar.calendarList.list({ maxResults: 1 });
|
|
349
|
+
report.services.calendar = { status: 'ok' };
|
|
350
|
+
} catch (err) {
|
|
351
|
+
report.services.calendar = { status: 'error', error: err.message };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Forms — just check scope presence
|
|
355
|
+
report.services.forms = {
|
|
356
|
+
status: SCOPES.some(s => s.includes('forms')) ? 'configured' : 'no_scope',
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Docs/Sheets — covered by Drive auth
|
|
360
|
+
report.services.docs = { status: report.services.drive.status === 'ok' ? 'ok (via Drive auth)' : 'unknown' };
|
|
361
|
+
report.services.sheets = { status: report.services.drive.status === 'ok' ? 'ok (via Drive auth)' : 'unknown' };
|
|
362
|
+
} else {
|
|
363
|
+
report.services = { note: 'Skipped — auth not available' };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// --- Config summary ---
|
|
367
|
+
const configDir = getConfigDir();
|
|
368
|
+
report.config = {
|
|
369
|
+
configDir,
|
|
370
|
+
profile: process.env.GOOGLE_MCP_PROFILE || '(default)',
|
|
371
|
+
tokenPath,
|
|
372
|
+
credentialSource: process.env.GOOGLE_CLIENT_ID ? 'environment' : 'file',
|
|
373
|
+
scopes: SCOPES,
|
|
374
|
+
logFile: process.env.GOOGLE_MCP_LOG_FILE || '(not set)',
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// --- Recent logs ---
|
|
378
|
+
const logFilePath = process.env.GOOGLE_MCP_LOG_FILE === '1'
|
|
379
|
+
? path.join(configDir, 'server.log')
|
|
380
|
+
: process.env.GOOGLE_MCP_LOG_FILE;
|
|
381
|
+
if (logFilePath) {
|
|
382
|
+
try {
|
|
383
|
+
const logContent = await fs.readFile(logFilePath, 'utf8');
|
|
384
|
+
const lines = logContent.trimEnd().split('\n');
|
|
385
|
+
report.recentLogs = lines.slice(-20);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
report.recentLogs = err.code === 'ENOENT' ? '(log file not found)' : `(error reading log: ${err.message})`;
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
report.recentLogs = '(file logging not enabled — set GOOGLE_MCP_LOG_FILE to enable)';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// --- Environment ---
|
|
394
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
395
|
+
let pkgVersion = 'unknown';
|
|
396
|
+
try {
|
|
397
|
+
const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
|
|
398
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
399
|
+
pkgVersion = pkg.version;
|
|
400
|
+
} catch {}
|
|
401
|
+
report.environment = {
|
|
402
|
+
serverVersion: pkgVersion,
|
|
403
|
+
nodeVersion: process.version,
|
|
404
|
+
platform: process.platform,
|
|
405
|
+
osRelease: os.release(),
|
|
406
|
+
arch: process.arch,
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
return JSON.stringify(report, null, 2);
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// --- Feedback tool (always available) ---
|
|
414
|
+
server.addTool({
|
|
415
|
+
name: 'feedback',
|
|
416
|
+
description:
|
|
417
|
+
'Submit feedback or a bug report for google-tools-mcp. Automatically collects diagnostic info, then files the issue via the GitHub CLI (`gh`) if available, or falls back to opening a pre-filled GitHub issue URL in the user\'s browser.',
|
|
418
|
+
parameters: z.object({
|
|
419
|
+
type: z.enum(['bug', 'feature']).describe('Type of feedback'),
|
|
420
|
+
title: z.string().describe('Short summary'),
|
|
421
|
+
description: z.string().describe('Detailed description of the issue or feature request'),
|
|
422
|
+
}),
|
|
423
|
+
execute: async (args) => {
|
|
424
|
+
// Collect diagnostics
|
|
425
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
426
|
+
let pkgVersion = 'unknown';
|
|
427
|
+
try {
|
|
428
|
+
const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
|
|
429
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
430
|
+
pkgVersion = pkg.version;
|
|
431
|
+
} catch {}
|
|
432
|
+
|
|
433
|
+
let authStatus = 'unknown';
|
|
434
|
+
const tokenPath = getTokenPath();
|
|
435
|
+
try {
|
|
436
|
+
await fs.access(tokenPath);
|
|
437
|
+
const client = getAuthClientIfReady();
|
|
438
|
+
if (client) {
|
|
439
|
+
try {
|
|
440
|
+
await client.refreshAccessToken();
|
|
441
|
+
authStatus = 'valid';
|
|
442
|
+
} catch {
|
|
443
|
+
authStatus = 'expired_or_revoked';
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
authStatus = 'not_initialized';
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
authStatus = 'missing';
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const enabledScopes = SCOPES.map(s => s.split('/').pop());
|
|
453
|
+
|
|
454
|
+
// Build markdown body
|
|
455
|
+
const diagnostics = [
|
|
456
|
+
`- **Server version:** ${pkgVersion}`,
|
|
457
|
+
`- **Node version:** ${process.version}`,
|
|
458
|
+
`- **OS:** ${process.platform} ${os.release()} (${process.arch})`,
|
|
459
|
+
`- **Auth status:** ${authStatus}`,
|
|
460
|
+
`- **Scopes:** ${enabledScopes.join(', ')}`,
|
|
461
|
+
].join('\n');
|
|
462
|
+
|
|
463
|
+
const body = [
|
|
464
|
+
`## Description`,
|
|
465
|
+
``,
|
|
466
|
+
args.description,
|
|
467
|
+
``,
|
|
468
|
+
`<details>`,
|
|
469
|
+
`<summary>Diagnostic Info</summary>`,
|
|
470
|
+
``,
|
|
471
|
+
diagnostics,
|
|
472
|
+
``,
|
|
473
|
+
`</details>`,
|
|
474
|
+
].join('\n');
|
|
475
|
+
|
|
476
|
+
const label = args.type === 'bug' ? 'bug' : 'enhancement';
|
|
477
|
+
|
|
478
|
+
// Try gh CLI first
|
|
479
|
+
const ghResult = await tryGhCli(args.title, body, label);
|
|
480
|
+
if (ghResult.ok) {
|
|
481
|
+
return JSON.stringify({
|
|
482
|
+
method: 'gh-cli',
|
|
483
|
+
issueUrl: ghResult.issueUrl,
|
|
484
|
+
note: 'Issue filed successfully via GitHub CLI.',
|
|
485
|
+
}, null, 2);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Fallback: open pre-filled GitHub issue URL in the user's browser
|
|
489
|
+
const params = new URLSearchParams({
|
|
490
|
+
title: args.title,
|
|
491
|
+
body,
|
|
492
|
+
labels: label,
|
|
493
|
+
});
|
|
494
|
+
const url = `https://github.com/${REPO}/issues/new?${params.toString()}`;
|
|
495
|
+
const opened = await openBrowser(url);
|
|
496
|
+
|
|
497
|
+
return JSON.stringify({
|
|
498
|
+
method: 'browser-fallback',
|
|
499
|
+
ghCliUnavailableReason: ghResult.reason,
|
|
500
|
+
url,
|
|
501
|
+
browserOpened: opened,
|
|
502
|
+
markdown: body,
|
|
503
|
+
note: url.length > 8000
|
|
504
|
+
? 'The URL may be too long for some browsers. Use the markdown body to create the issue manually.'
|
|
505
|
+
: opened
|
|
506
|
+
? 'Opened the pre-filled GitHub issue in your browser. Click "Submit new issue" to file it.'
|
|
507
|
+
: 'Could not auto-open browser. Please open the URL manually.',
|
|
508
|
+
}, null, 2);
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
}
|