google-tools-mcp 1.0.10 → 1.0.12
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 +16 -3
- package/dist/auth.js +19 -2
- package/dist/clients.js +45 -0
- package/dist/index.js +1 -1
- package/dist/tools/calendar/manageEvent.js +8 -0
- package/dist/tools/calendar/moveEvent.js +6 -0
- package/dist/tools/index.js +22 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,15 +85,28 @@ Add the credentials directly to your MCP configuration:
|
|
|
85
85
|
|
|
86
86
|
#### Claude Code (recommended)
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
**User-scope** (available in all projects):
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
claude mcp add -s user google -- npx -y google-tools-mcp
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Project-scope** (available only in the current project):
|
|
89
95
|
|
|
90
96
|
```bash
|
|
91
97
|
claude mcp add google -- npx -y google-tools-mcp
|
|
92
98
|
```
|
|
93
99
|
|
|
94
|
-
|
|
100
|
+
With env vars (Option C):
|
|
95
101
|
|
|
96
102
|
```bash
|
|
103
|
+
# User-scope
|
|
104
|
+
claude mcp add -s user google \
|
|
105
|
+
-e GOOGLE_CLIENT_ID=your-client-id \
|
|
106
|
+
-e GOOGLE_CLIENT_SECRET=your-client-secret \
|
|
107
|
+
-- npx -y google-tools-mcp
|
|
108
|
+
|
|
109
|
+
# Project-scope
|
|
97
110
|
claude mcp add google \
|
|
98
111
|
-e GOOGLE_CLIENT_ID=your-client-id \
|
|
99
112
|
-e GOOGLE_CLIENT_SECRET=your-client-secret \
|
|
@@ -105,7 +118,7 @@ claude mcp add google \
|
|
|
105
118
|
Via the `claude` CLI:
|
|
106
119
|
|
|
107
120
|
```bash
|
|
108
|
-
claude mcp add google \
|
|
121
|
+
claude mcp add -s user google \
|
|
109
122
|
-e GOOGLE_MCP_PROFILE=myprofile \
|
|
110
123
|
-- npx -y google-tools-mcp
|
|
111
124
|
```
|
package/dist/auth.js
CHANGED
|
@@ -269,8 +269,25 @@ export async function authorize() {
|
|
|
269
269
|
logger.info('Attempting OAuth 2.0 authentication...');
|
|
270
270
|
const client = await loadSavedCredentialsIfExist();
|
|
271
271
|
if (client) {
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
// Proactively refresh to verify the token is still valid
|
|
273
|
+
try {
|
|
274
|
+
const { credentials } = await client.refreshAccessToken();
|
|
275
|
+
client.setCredentials(credentials);
|
|
276
|
+
if (credentials.refresh_token) {
|
|
277
|
+
await saveCredentials(client);
|
|
278
|
+
}
|
|
279
|
+
logger.info('Using saved credentials (token refreshed successfully).');
|
|
280
|
+
return client;
|
|
281
|
+
} catch (err) {
|
|
282
|
+
const isInvalidGrant = err.message?.includes('invalid_grant') ||
|
|
283
|
+
err.response?.data?.error === 'invalid_grant';
|
|
284
|
+
if (isInvalidGrant) {
|
|
285
|
+
logger.warn('Saved refresh token is invalid/revoked. Starting re-authentication...');
|
|
286
|
+
try { await fs.unlink(getTokenPath()); } catch {}
|
|
287
|
+
return authenticate();
|
|
288
|
+
}
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
274
291
|
}
|
|
275
292
|
logger.info('No saved token found. Starting interactive authentication flow...');
|
|
276
293
|
return authenticate();
|
package/dist/clients.js
CHANGED
|
@@ -31,6 +31,33 @@ async function ensureAuth() {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Re-authenticate and rebuild all API clients after an invalid_grant error.
|
|
36
|
+
*/
|
|
37
|
+
async function reauthorize() {
|
|
38
|
+
logger.info('Re-authorizing after invalid_grant...');
|
|
39
|
+
authClient = null;
|
|
40
|
+
googleDocs = null;
|
|
41
|
+
googleDrive = null;
|
|
42
|
+
googleSheets = null;
|
|
43
|
+
googleScript = null;
|
|
44
|
+
gmailClient = null;
|
|
45
|
+
calendarClient = null;
|
|
46
|
+
formsClient = null;
|
|
47
|
+
authClient = await authorize();
|
|
48
|
+
logger.info('Re-authorization successful.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if an error is an invalid_grant error (expired/revoked refresh token).
|
|
53
|
+
*/
|
|
54
|
+
function isInvalidGrantError(error) {
|
|
55
|
+
if (!error) return false;
|
|
56
|
+
const msg = error.message || '';
|
|
57
|
+
const code = error.response?.data?.error || error.code || '';
|
|
58
|
+
return msg.includes('invalid_grant') || code === 'invalid_grant';
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
// --- GDrive clients ---
|
|
35
62
|
export async function initializeGoogleClient() {
|
|
36
63
|
if (googleDocs && googleDrive && googleSheets)
|
|
@@ -79,6 +106,24 @@ export function resetClients() {
|
|
|
79
106
|
formsClient = null;
|
|
80
107
|
}
|
|
81
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Execute a function with automatic re-auth on invalid_grant errors.
|
|
111
|
+
* Wraps any API call so that if the refresh token has been revoked mid-session,
|
|
112
|
+
* we re-authenticate transparently and retry once.
|
|
113
|
+
*/
|
|
114
|
+
export async function withAuthRetry(fn) {
|
|
115
|
+
try {
|
|
116
|
+
return await fn();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (isInvalidGrantError(error)) {
|
|
119
|
+
logger.warn('Got invalid_grant during API call. Re-authenticating...');
|
|
120
|
+
await reauthorize();
|
|
121
|
+
return await fn();
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
82
127
|
// --- Individual client getters ---
|
|
83
128
|
export async function getDocsClient() {
|
|
84
129
|
const { googleDocs: docs } = await initializeGoogleClient();
|
package/dist/index.js
CHANGED
|
@@ -69,6 +69,11 @@ export function register(server) {
|
|
|
69
69
|
.array(z.string())
|
|
70
70
|
.optional()
|
|
71
71
|
.describe('RRULE recurrence rules (e.g. ["RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR"]). Only for create.'),
|
|
72
|
+
send_updates: z
|
|
73
|
+
.enum(['all', 'externalOnly', 'none'])
|
|
74
|
+
.optional()
|
|
75
|
+
.default('all')
|
|
76
|
+
.describe('Who to send email notifications to: "all" = all attendees (default), "externalOnly" = non-Google Calendar attendees only, "none" = no emails.'),
|
|
72
77
|
}),
|
|
73
78
|
execute: async (args, { log }) => {
|
|
74
79
|
const calendar = await getCalendarClient();
|
|
@@ -80,6 +85,7 @@ export function register(server) {
|
|
|
80
85
|
await calendar.events.delete({
|
|
81
86
|
calendarId: args.calendar_id,
|
|
82
87
|
eventId: args.event_id,
|
|
88
|
+
sendUpdates: args.send_updates,
|
|
83
89
|
});
|
|
84
90
|
return JSON.stringify({ success: true, message: `Event ${args.event_id} deleted.` });
|
|
85
91
|
}
|
|
@@ -96,6 +102,7 @@ export function register(server) {
|
|
|
96
102
|
calendarId: args.calendar_id,
|
|
97
103
|
requestBody: eventBody,
|
|
98
104
|
conferenceDataVersion: args.add_google_meet ? 1 : 0,
|
|
105
|
+
sendUpdates: args.send_updates,
|
|
99
106
|
};
|
|
100
107
|
|
|
101
108
|
const response = await calendar.events.insert(params);
|
|
@@ -119,6 +126,7 @@ export function register(server) {
|
|
|
119
126
|
eventId: args.event_id,
|
|
120
127
|
requestBody: eventBody,
|
|
121
128
|
conferenceDataVersion: args.add_google_meet !== undefined ? 1 : 0,
|
|
129
|
+
sendUpdates: args.send_updates,
|
|
122
130
|
};
|
|
123
131
|
|
|
124
132
|
const response = await calendar.events.update(params);
|
|
@@ -17,6 +17,11 @@ export function register(server) {
|
|
|
17
17
|
destination_calendar_id: z
|
|
18
18
|
.string()
|
|
19
19
|
.describe('Calendar ID to move the event to.'),
|
|
20
|
+
send_updates: z
|
|
21
|
+
.enum(['all', 'externalOnly', 'none'])
|
|
22
|
+
.optional()
|
|
23
|
+
.default('all')
|
|
24
|
+
.describe('Who to send email notifications to: "all" = all attendees (default), "externalOnly" = non-Google Calendar attendees only, "none" = no emails.'),
|
|
20
25
|
}),
|
|
21
26
|
execute: async (args, { log }) => {
|
|
22
27
|
const calendar = await getCalendarClient();
|
|
@@ -29,6 +34,7 @@ export function register(server) {
|
|
|
29
34
|
calendarId: args.source_calendar_id,
|
|
30
35
|
eventId: args.event_id,
|
|
31
36
|
destination: args.destination_calendar_id,
|
|
37
|
+
sendUpdates: args.send_updates,
|
|
32
38
|
});
|
|
33
39
|
|
|
34
40
|
const event = response.data;
|
package/dist/tools/index.js
CHANGED
|
@@ -6,9 +6,26 @@ import * as fs from 'fs/promises';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { getTokenPath } from '../auth.js';
|
|
9
|
-
import { resetClients } from '../clients.js';
|
|
9
|
+
import { resetClients, withAuthRetry } from '../clients.js';
|
|
10
10
|
import { logger } from '../logger.js';
|
|
11
11
|
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Wrap server.addTool so every tool's execute() auto-retries on invalid_grant.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
function wrapServerWithAuthRetry(server) {
|
|
16
|
+
const originalAddTool = server.addTool.bind(server);
|
|
17
|
+
server.addTool = function (toolDef) {
|
|
18
|
+
const originalExecute = toolDef.execute;
|
|
19
|
+
if (originalExecute) {
|
|
20
|
+
toolDef.execute = function (...args) {
|
|
21
|
+
return withAuthRetry(() => originalExecute.apply(this, args));
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return originalAddTool(toolDef);
|
|
25
|
+
};
|
|
26
|
+
return server;
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
// --- Category registry ---
|
|
13
30
|
const CATEGORIES = {
|
|
14
31
|
files: {
|
|
@@ -77,9 +94,12 @@ const CATEGORIES = {
|
|
|
77
94
|
// Public: register all tools eagerly, plus the logout utility.
|
|
78
95
|
// ---------------------------------------------------------------------------
|
|
79
96
|
export async function registerAllTools(server) {
|
|
97
|
+
// Wrap server so every tool auto-retries on invalid_grant (expired refresh token)
|
|
98
|
+
const wrappedServer = wrapServerWithAuthRetry(server);
|
|
99
|
+
|
|
80
100
|
// Load every category
|
|
81
101
|
for (const [name, { loader }] of Object.entries(CATEGORIES)) {
|
|
82
|
-
await loader(
|
|
102
|
+
await loader(wrappedServer);
|
|
83
103
|
}
|
|
84
104
|
logger.info(`Loaded all ${Object.keys(CATEGORIES).length} categories at startup.`);
|
|
85
105
|
|