google-tools-mcp 1.2.8 → 1.2.10
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 +21 -6
- package/dist/auth.js +343 -325
- package/dist/clients.js +293 -213
- package/dist/index.js +85 -74
- package/dist/setup.js +453 -235
- package/dist/tools/sheets/deleteColumns.js +66 -0
- package/dist/tools/sheets/index.js +2 -0
- package/dist/tools/sheets/ungroupAllRows.js +6 -2
- package/package.json +1 -1
package/dist/clients.js
CHANGED
|
@@ -1,213 +1,293 @@
|
|
|
1
|
-
// Combined clients for google-tools-mcp.
|
|
2
|
-
// Lazy-loads all Google API clients (Docs, Drive, Sheets, Script, Gmail) on first use.
|
|
3
|
-
import { google } from 'googleapis';
|
|
4
|
-
import { UserError } from 'fastmcp';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
let
|
|
10
|
-
let
|
|
11
|
-
let
|
|
12
|
-
let
|
|
13
|
-
let
|
|
14
|
-
let
|
|
15
|
-
let
|
|
16
|
-
let
|
|
17
|
-
let
|
|
18
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (!
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (!
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (
|
|
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
|
-
|
|
1
|
+
// Combined clients for google-tools-mcp.
|
|
2
|
+
// Lazy-loads all Google API clients (Docs, Drive, Sheets, Script, Gmail) on first use.
|
|
3
|
+
import { google } from 'googleapis';
|
|
4
|
+
import { UserError } from 'fastmcp';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { authorize } from './auth.js';
|
|
7
|
+
import { logger } from './logger.js';
|
|
8
|
+
|
|
9
|
+
let authClient = null;
|
|
10
|
+
let googleDocs = null;
|
|
11
|
+
let googleDrive = null;
|
|
12
|
+
let googleSheets = null;
|
|
13
|
+
let googleScript = null;
|
|
14
|
+
let gmailClient = null;
|
|
15
|
+
let calendarClient = null;
|
|
16
|
+
let formsClient = null;
|
|
17
|
+
let slidesClient = null;
|
|
18
|
+
let tasksClient = null;
|
|
19
|
+
|
|
20
|
+
async function ensureAuth() {
|
|
21
|
+
if (authClient) return;
|
|
22
|
+
try {
|
|
23
|
+
logger.info('Attempting to authorize Google API client...');
|
|
24
|
+
authClient = await authorize();
|
|
25
|
+
logger.info('Google API client authorized successfully.');
|
|
26
|
+
} catch (error) {
|
|
27
|
+
logger.error('Failed to initialize Google API client:', error);
|
|
28
|
+
authClient = null;
|
|
29
|
+
throw new UserError(
|
|
30
|
+
'Google authentication required. A browser window should have opened automatically. ' +
|
|
31
|
+
'If not, run: npx google-tools-mcp auth\n\n' +
|
|
32
|
+
'Details: ' + (error.message || error)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Re-authenticate and rebuild all API clients after an invalid_grant error.
|
|
39
|
+
*/
|
|
40
|
+
async function reauthorize() {
|
|
41
|
+
logger.info('Re-authorizing after invalid_grant...');
|
|
42
|
+
authClient = null;
|
|
43
|
+
googleDocs = null;
|
|
44
|
+
googleDrive = null;
|
|
45
|
+
googleSheets = null;
|
|
46
|
+
googleScript = null;
|
|
47
|
+
gmailClient = null;
|
|
48
|
+
calendarClient = null;
|
|
49
|
+
formsClient = null;
|
|
50
|
+
slidesClient = null;
|
|
51
|
+
tasksClient = null;
|
|
52
|
+
authClient = await authorize();
|
|
53
|
+
logger.info('Re-authorization successful.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if an error is an invalid_grant error (expired/revoked refresh token).
|
|
58
|
+
*/
|
|
59
|
+
function isInvalidGrantError(error) {
|
|
60
|
+
if (!error) return false;
|
|
61
|
+
const msg = error.message || '';
|
|
62
|
+
const code = error.response?.data?.error || error.code || '';
|
|
63
|
+
return msg.includes('invalid_grant') || code === 'invalid_grant';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Detect "API has not been used / is disabled" errors from Google APIs.
|
|
68
|
+
* Triggered when a required Google API was never enabled in the user's project.
|
|
69
|
+
*/
|
|
70
|
+
function isApiNotEnabledError(error) {
|
|
71
|
+
if (!error) return false;
|
|
72
|
+
const msg = error.message || '';
|
|
73
|
+
if (/has not been used in project .* before or it is disabled/i.test(msg)) return true;
|
|
74
|
+
if (/SERVICE_DISABLED/i.test(msg)) return true;
|
|
75
|
+
const details = error.response?.data?.error?.details;
|
|
76
|
+
if (Array.isArray(details)) {
|
|
77
|
+
for (const d of details) {
|
|
78
|
+
if (d?.reason === 'SERVICE_DISABLED') return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Pull the API name, project ID, and enable URL out of an API-disabled error.
|
|
86
|
+
* Returns null if extraction fails.
|
|
87
|
+
*/
|
|
88
|
+
function extractApiEnableInfo(error) {
|
|
89
|
+
const msg = error?.message || '';
|
|
90
|
+
let api = null;
|
|
91
|
+
let projectId = null;
|
|
92
|
+
let enableUrl = null;
|
|
93
|
+
const urlMatch = msg.match(/https:\/\/console\.(?:developers|cloud)\.google\.com\/[^\s)]+/);
|
|
94
|
+
if (urlMatch) enableUrl = urlMatch[0];
|
|
95
|
+
const apiMatch = msg.match(/api\/([a-z0-9.-]+\.googleapis\.com)/i)
|
|
96
|
+
|| msg.match(/([a-z0-9.-]+\.googleapis\.com)/i);
|
|
97
|
+
if (apiMatch) api = apiMatch[1];
|
|
98
|
+
const projMatch = msg.match(/project=(\d+)/) || msg.match(/project (\d+)/);
|
|
99
|
+
if (projMatch) projectId = projMatch[1];
|
|
100
|
+
const details = error?.response?.data?.error?.details;
|
|
101
|
+
if (Array.isArray(details)) {
|
|
102
|
+
for (const d of details) {
|
|
103
|
+
if (d?.metadata?.service && !api) api = d.metadata.service;
|
|
104
|
+
if (d?.metadata?.consumer && !projectId) {
|
|
105
|
+
const m = d.metadata.consumer.match(/projects\/(\d+)/);
|
|
106
|
+
if (m) projectId = m[1];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!enableUrl && api) {
|
|
111
|
+
enableUrl = `https://console.cloud.google.com/apis/api/${api}/overview${projectId ? `?project=${projectId}` : ''}`;
|
|
112
|
+
}
|
|
113
|
+
if (!api && !enableUrl) return null;
|
|
114
|
+
return { api, projectId, enableUrl };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function openBrowser(url) {
|
|
118
|
+
const platform = process.platform;
|
|
119
|
+
let cmd;
|
|
120
|
+
if (platform === 'win32') cmd = `start "" "${url}"`;
|
|
121
|
+
else if (platform === 'darwin') cmd = `open "${url}"`;
|
|
122
|
+
else cmd = `xdg-open "${url}"`;
|
|
123
|
+
exec(cmd, () => { });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Track APIs we've auto-opened this session so we don't spam the browser.
|
|
127
|
+
const openedApiEnableUrls = new Set();
|
|
128
|
+
|
|
129
|
+
// --- GDrive clients ---
|
|
130
|
+
export async function initializeGoogleClient() {
|
|
131
|
+
if (googleDocs && googleDrive && googleSheets)
|
|
132
|
+
return { authClient, googleDocs, googleDrive, googleSheets, googleScript };
|
|
133
|
+
await ensureAuth();
|
|
134
|
+
if (!googleDocs) googleDocs = google.docs({ version: 'v1', auth: authClient });
|
|
135
|
+
if (!googleDrive) googleDrive = google.drive({ version: 'v3', auth: authClient });
|
|
136
|
+
if (!googleSheets) googleSheets = google.sheets({ version: 'v4', auth: authClient });
|
|
137
|
+
if (!googleScript) googleScript = google.script({ version: 'v1', auth: authClient });
|
|
138
|
+
return { authClient, googleDocs, googleDrive, googleSheets, googleScript };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- Gmail client ---
|
|
142
|
+
export async function initializeGmailClient() {
|
|
143
|
+
if (gmailClient) return { authClient, gmailClient };
|
|
144
|
+
await ensureAuth();
|
|
145
|
+
if (!gmailClient) gmailClient = google.gmail({ version: 'v1', auth: authClient });
|
|
146
|
+
return { authClient, gmailClient };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- Calendar client ---
|
|
150
|
+
export async function initializeCalendarClient() {
|
|
151
|
+
if (calendarClient) return { authClient, calendarClient };
|
|
152
|
+
await ensureAuth();
|
|
153
|
+
if (!calendarClient) calendarClient = google.calendar({ version: 'v3', auth: authClient });
|
|
154
|
+
return { authClient, calendarClient };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Forms client ---
|
|
158
|
+
export async function initializeFormsClient() {
|
|
159
|
+
if (formsClient) return { authClient, formsClient };
|
|
160
|
+
await ensureAuth();
|
|
161
|
+
if (!formsClient) formsClient = google.forms({ version: 'v1', auth: authClient });
|
|
162
|
+
return { authClient, formsClient };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- Slides client ---
|
|
166
|
+
export async function initializeSlidesClient() {
|
|
167
|
+
if (slidesClient) return { authClient, slidesClient };
|
|
168
|
+
await ensureAuth();
|
|
169
|
+
if (!slidesClient) slidesClient = google.slides({ version: 'v1', auth: authClient });
|
|
170
|
+
return { authClient, slidesClient };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --- Get auth client without triggering init (for diagnostics) ---
|
|
174
|
+
export function getAuthClientIfReady() {
|
|
175
|
+
return authClient;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- Reset all clients (used by logout) ---
|
|
179
|
+
export function resetClients() {
|
|
180
|
+
authClient = null;
|
|
181
|
+
googleDocs = null;
|
|
182
|
+
googleDrive = null;
|
|
183
|
+
googleSheets = null;
|
|
184
|
+
googleScript = null;
|
|
185
|
+
gmailClient = null;
|
|
186
|
+
calendarClient = null;
|
|
187
|
+
formsClient = null;
|
|
188
|
+
slidesClient = null;
|
|
189
|
+
tasksClient = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Execute a function with automatic re-auth on invalid_grant errors.
|
|
194
|
+
* Wraps any API call so that if the refresh token has been revoked mid-session,
|
|
195
|
+
* we re-authenticate transparently and retry once.
|
|
196
|
+
*/
|
|
197
|
+
export async function withAuthRetry(fn) {
|
|
198
|
+
try {
|
|
199
|
+
return await fn();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (isInvalidGrantError(error)) {
|
|
202
|
+
logger.warn('Got invalid_grant during API call. Re-authenticating...');
|
|
203
|
+
await reauthorize();
|
|
204
|
+
return await fn();
|
|
205
|
+
}
|
|
206
|
+
if (isApiNotEnabledError(error)) {
|
|
207
|
+
const info = extractApiEnableInfo(error);
|
|
208
|
+
if (info?.enableUrl) {
|
|
209
|
+
const apiLabel = info.api || 'a required Google API';
|
|
210
|
+
if (!openedApiEnableUrls.has(info.enableUrl)) {
|
|
211
|
+
openedApiEnableUrls.add(info.enableUrl);
|
|
212
|
+
logger.warn(`Auto-opening enable URL for ${apiLabel}: ${info.enableUrl}`);
|
|
213
|
+
openBrowser(info.enableUrl);
|
|
214
|
+
}
|
|
215
|
+
throw new UserError(
|
|
216
|
+
`${apiLabel} is not enabled${info.projectId ? ` for project ${info.projectId}` : ''}. ` +
|
|
217
|
+
`A browser window was opened to the enable page — click "Enable", wait ~30 seconds for it to propagate, then retry your request.\n\n` +
|
|
218
|
+
`Enable URL: ${info.enableUrl}`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// --- Individual client getters ---
|
|
227
|
+
export async function getDocsClient() {
|
|
228
|
+
const { googleDocs: docs } = await initializeGoogleClient();
|
|
229
|
+
if (!docs) throw new UserError('Google Docs client is not initialized.');
|
|
230
|
+
return docs;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function getDriveClient() {
|
|
234
|
+
const { googleDrive: drive } = await initializeGoogleClient();
|
|
235
|
+
if (!drive) throw new UserError('Google Drive client is not initialized.');
|
|
236
|
+
return drive;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function getSheetsClient() {
|
|
240
|
+
const { googleSheets: sheets } = await initializeGoogleClient();
|
|
241
|
+
if (!sheets) throw new UserError('Google Sheets client is not initialized.');
|
|
242
|
+
return sheets;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function getAuthClient() {
|
|
246
|
+
const { authClient: client } = await initializeGoogleClient();
|
|
247
|
+
if (!client) throw new UserError('Auth client is not initialized.');
|
|
248
|
+
return client;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function getScriptClient() {
|
|
252
|
+
const { googleScript: script } = await initializeGoogleClient();
|
|
253
|
+
if (!script) throw new UserError('Google Script client is not initialized.');
|
|
254
|
+
return script;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function getGmailClient() {
|
|
258
|
+
const { gmailClient: gmail } = await initializeGmailClient();
|
|
259
|
+
if (!gmail) throw new UserError('Gmail client is not initialized.');
|
|
260
|
+
return gmail;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function getCalendarClient() {
|
|
264
|
+
const { calendarClient: calendar } = await initializeCalendarClient();
|
|
265
|
+
if (!calendar) throw new UserError('Google Calendar client is not initialized.');
|
|
266
|
+
return calendar;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function getFormsClient() {
|
|
270
|
+
const { formsClient: forms } = await initializeFormsClient();
|
|
271
|
+
if (!forms) throw new UserError('Google Forms client is not initialized.');
|
|
272
|
+
return forms;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function getSlidesClient() {
|
|
276
|
+
const { slidesClient: slides } = await initializeSlidesClient();
|
|
277
|
+
if (!slides) throw new UserError('Google Slides client is not initialized.');
|
|
278
|
+
return slides;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// --- Tasks client ---
|
|
282
|
+
export async function initializeTasksClient() {
|
|
283
|
+
if (tasksClient) return { authClient, tasksClient };
|
|
284
|
+
await ensureAuth();
|
|
285
|
+
if (!tasksClient) tasksClient = google.tasks({ version: 'v1', auth: authClient });
|
|
286
|
+
return { authClient, tasksClient };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function getTasksClient() {
|
|
290
|
+
const { tasksClient: tasks } = await initializeTasksClient();
|
|
291
|
+
if (!tasks) throw new UserError('Google Tasks client is not initialized.');
|
|
292
|
+
return tasks;
|
|
293
|
+
}
|