google-tools-mcp 1.0.13 → 1.0.15
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 +1 -0
- package/dist/auth.js +7 -2
- package/dist/clients.js +5 -0
- package/dist/index.js +13 -1
- package/dist/logger.js +62 -20
- package/dist/tools/calendar/manageEvent.js +2 -2
- package/dist/tools/index.js +214 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -235,6 +235,7 @@ Google Forms — create/read forms, manage responses, and publish settings.
|
|
|
235
235
|
| `GOOGLE_CLIENT_SECRET` | No* | OAuth 2.0 Client Secret |
|
|
236
236
|
| `GOOGLE_MCP_PROFILE` | No | Profile name for multi-account support (see above) |
|
|
237
237
|
| `LOG_LEVEL` | No | `debug`, `info`, `warn`, `error`, or `silent` |
|
|
238
|
+
| `GOOGLE_MCP_LOG_FILE` | No | Set to `1` to log to `~/.config/google-tools-mcp/server.log`, or set to a custom file path |
|
|
238
239
|
| `SERVICE_ACCOUNT_PATH` | No | Path to service account JSON key (alternative to OAuth) |
|
|
239
240
|
| `GOOGLE_IMPERSONATE_USER` | No | Email to impersonate with service account |
|
|
240
241
|
|
package/dist/auth.js
CHANGED
|
@@ -259,7 +259,7 @@ async function authenticate() {
|
|
|
259
259
|
// ---------------------------------------------------------------------------
|
|
260
260
|
// Public API
|
|
261
261
|
// ---------------------------------------------------------------------------
|
|
262
|
-
export { getTokenPath };
|
|
262
|
+
export { getTokenPath, getConfigDir, SCOPES };
|
|
263
263
|
|
|
264
264
|
export async function authorize() {
|
|
265
265
|
if (process.env.SERVICE_ACCOUNT_PATH) {
|
|
@@ -271,12 +271,16 @@ export async function authorize() {
|
|
|
271
271
|
if (client) {
|
|
272
272
|
// Proactively refresh to verify the token is still valid
|
|
273
273
|
try {
|
|
274
|
+
logger.info('Refreshing access token...');
|
|
274
275
|
const { credentials } = await client.refreshAccessToken();
|
|
275
276
|
client.setCredentials(credentials);
|
|
276
277
|
if (credentials.refresh_token) {
|
|
277
278
|
await saveCredentials(client);
|
|
278
279
|
}
|
|
279
|
-
|
|
280
|
+
const expiryDate = credentials.expiry_date
|
|
281
|
+
? new Date(credentials.expiry_date).toISOString()
|
|
282
|
+
: 'unknown';
|
|
283
|
+
logger.info(`Token refreshed successfully. Expires: ${expiryDate}`);
|
|
280
284
|
return client;
|
|
281
285
|
} catch (err) {
|
|
282
286
|
const isInvalidGrant = err.message?.includes('invalid_grant') ||
|
|
@@ -286,6 +290,7 @@ export async function authorize() {
|
|
|
286
290
|
try { await fs.unlink(getTokenPath()); } catch {}
|
|
287
291
|
return authenticate();
|
|
288
292
|
}
|
|
293
|
+
logger.error('Token refresh failed:', err.message || err);
|
|
289
294
|
throw err;
|
|
290
295
|
}
|
|
291
296
|
}
|
package/dist/clients.js
CHANGED
|
@@ -94,6 +94,11 @@ export async function initializeFormsClient() {
|
|
|
94
94
|
return { authClient, formsClient };
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// --- Get auth client without triggering init (for diagnostics) ---
|
|
98
|
+
export function getAuthClientIfReady() {
|
|
99
|
+
return authClient;
|
|
100
|
+
}
|
|
101
|
+
|
|
97
102
|
// --- Reset all clients (used by logout) ---
|
|
98
103
|
export function resetClients() {
|
|
99
104
|
authClient = null;
|
package/dist/index.js
CHANGED
|
@@ -24,14 +24,26 @@ if (process.argv[2] === 'auth') {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// ---
|
|
27
|
+
// --- Process lifecycle logging ---
|
|
28
28
|
process.on('uncaughtException', (error) => {
|
|
29
29
|
logger.error('Uncaught Exception:', error);
|
|
30
30
|
});
|
|
31
31
|
process.on('unhandledRejection', (reason, _promise) => {
|
|
32
32
|
logger.error('Unhandled Promise Rejection:', reason);
|
|
33
33
|
});
|
|
34
|
+
process.on('SIGINT', () => {
|
|
35
|
+
logger.info('Received SIGINT — shutting down.');
|
|
36
|
+
process.exit(0);
|
|
37
|
+
});
|
|
38
|
+
process.on('SIGTERM', () => {
|
|
39
|
+
logger.info('Received SIGTERM — shutting down.');
|
|
40
|
+
process.exit(0);
|
|
41
|
+
});
|
|
42
|
+
process.on('exit', (code) => {
|
|
43
|
+
logger.info(`Process exiting with code ${code}.`);
|
|
44
|
+
});
|
|
34
45
|
|
|
46
|
+
// --- Server startup ---
|
|
35
47
|
const server = new FastMCP({
|
|
36
48
|
name: 'google-tools-mcp',
|
|
37
49
|
version: '1.0.0',
|
package/dist/logger.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// Centralized logger with LOG_LEVEL support.
|
|
2
2
|
// All log output goes to stderr (stdout reserved for MCP protocol).
|
|
3
|
+
// If GOOGLE_MCP_LOG_FILE is set, logs are also appended to that file.
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as os from 'os';
|
|
7
|
+
|
|
3
8
|
const LOG_LEVELS = {
|
|
4
9
|
debug: 0,
|
|
5
10
|
info: 1,
|
|
@@ -21,25 +26,62 @@ export function refreshLogLevel() {
|
|
|
21
26
|
function shouldLog(level) {
|
|
22
27
|
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
|
|
23
28
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
|
|
30
|
+
// --- File logging ---
|
|
31
|
+
let logStream = null;
|
|
32
|
+
|
|
33
|
+
function getDefaultLogPath() {
|
|
34
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
35
|
+
const base = xdg || path.join(os.homedir(), '.config');
|
|
36
|
+
const baseDir = path.join(base, 'google-tools-mcp');
|
|
37
|
+
const profile = process.env.GOOGLE_MCP_PROFILE;
|
|
38
|
+
const dir = profile ? path.join(baseDir, profile) : baseDir;
|
|
39
|
+
return path.join(dir, 'server.log');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function initLogFile() {
|
|
43
|
+
if (logStream) return;
|
|
44
|
+
const logPath = process.env.GOOGLE_MCP_LOG_FILE === '1'
|
|
45
|
+
? getDefaultLogPath()
|
|
46
|
+
: process.env.GOOGLE_MCP_LOG_FILE;
|
|
47
|
+
if (!logPath) return;
|
|
48
|
+
try {
|
|
49
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
50
|
+
logStream = fs.createWriteStream(logPath, { flags: 'a' });
|
|
51
|
+
} catch {
|
|
52
|
+
// If we can't open the log file, continue without file logging
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
initLogFile();
|
|
56
|
+
|
|
57
|
+
function timestamp() {
|
|
58
|
+
return new Date().toISOString();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatArgs(args) {
|
|
62
|
+
return args.map(a => {
|
|
63
|
+
if (a instanceof Error) return a.stack || a.message;
|
|
64
|
+
if (typeof a === 'object') {
|
|
65
|
+
try { return JSON.stringify(a); } catch { return String(a); }
|
|
43
66
|
}
|
|
44
|
-
|
|
67
|
+
return String(a);
|
|
68
|
+
}).join(' ');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function log(level, args) {
|
|
72
|
+
if (!shouldLog(level)) return;
|
|
73
|
+
const tag = level.toUpperCase();
|
|
74
|
+
const ts = timestamp();
|
|
75
|
+
const msg = formatArgs(args);
|
|
76
|
+
console.error(`${ts} [${tag}] ${msg}`);
|
|
77
|
+
if (logStream) {
|
|
78
|
+
logStream.write(`${ts} [${tag}] ${msg}\n`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const logger = {
|
|
83
|
+
debug(...args) { log('debug', args); },
|
|
84
|
+
info(...args) { log('info', args); },
|
|
85
|
+
warn(...args) { log('warn', args); },
|
|
86
|
+
error(...args) { log('error', args); },
|
|
45
87
|
};
|
|
@@ -6,9 +6,9 @@ export function register(server) {
|
|
|
6
6
|
server.addTool({
|
|
7
7
|
name: 'manage_event',
|
|
8
8
|
description:
|
|
9
|
-
'Create, update, or delete a calendar event. Supports attendees, Google Meet, reminders, attachments, visibility, and transparency settings.',
|
|
9
|
+
'Create, update, or delete a calendar event. Supports attendees, Google Meet, reminders, attachments, visibility, and transparency settings. IMPORTANT: To modify an existing event, always use action "update" with the existing event_id. Never delete and recreate an event to make changes — this destroys the event ID, attendee RSVPs, and sent notifications.',
|
|
10
10
|
parameters: z.object({
|
|
11
|
-
action: z.enum(['create', 'update', 'delete']).describe('The operation to perform.'),
|
|
11
|
+
action: z.enum(['create', 'update', 'delete']).describe('The operation to perform. Use "update" to modify existing events — do not delete and recreate. "delete" should only be used to permanently remove an event, not to redo one.'),
|
|
12
12
|
calendar_id: z
|
|
13
13
|
.string()
|
|
14
14
|
.optional()
|
package/dist/tools/index.js
CHANGED
|
@@ -5,9 +5,11 @@ import { z } from 'zod';
|
|
|
5
5
|
import * as fs from 'fs/promises';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import { getTokenPath, getConfigDir, SCOPES } from '../auth.js';
|
|
10
|
+
import { resetClients, withAuthRetry, getAuthClientIfReady } from '../clients.js';
|
|
10
11
|
import { logger } from '../logger.js';
|
|
12
|
+
import { google } from 'googleapis';
|
|
11
13
|
|
|
12
14
|
// ---------------------------------------------------------------------------
|
|
13
15
|
// Wrap server.addTool so every tool's execute() auto-retries on invalid_grant.
|
|
@@ -140,4 +142,214 @@ export async function registerAllTools(server) {
|
|
|
140
142
|
});
|
|
141
143
|
},
|
|
142
144
|
});
|
|
145
|
+
|
|
146
|
+
// --- Troubleshoot tool (always available) ---
|
|
147
|
+
server.addTool({
|
|
148
|
+
name: 'troubleshoot',
|
|
149
|
+
description:
|
|
150
|
+
'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.',
|
|
151
|
+
parameters: z.object({}),
|
|
152
|
+
execute: async () => {
|
|
153
|
+
const report = { auth: {}, services: {}, config: {}, recentLogs: null, environment: {} };
|
|
154
|
+
|
|
155
|
+
// --- Auth status ---
|
|
156
|
+
const tokenPath = getTokenPath();
|
|
157
|
+
try {
|
|
158
|
+
const tokenContent = await fs.readFile(tokenPath, 'utf8');
|
|
159
|
+
const token = JSON.parse(tokenContent);
|
|
160
|
+
report.auth.tokenFile = 'present';
|
|
161
|
+
report.auth.hasRefreshToken = !!token.refresh_token;
|
|
162
|
+
report.auth.type = token.type || 'unknown';
|
|
163
|
+
} catch (err) {
|
|
164
|
+
report.auth.tokenFile = err.code === 'ENOENT' ? 'missing' : 'unreadable';
|
|
165
|
+
report.auth.hasRefreshToken = false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Try refreshing token
|
|
169
|
+
const client = getAuthClientIfReady();
|
|
170
|
+
if (client) {
|
|
171
|
+
try {
|
|
172
|
+
const { credentials } = await client.refreshAccessToken();
|
|
173
|
+
client.setCredentials(credentials);
|
|
174
|
+
report.auth.status = 'valid';
|
|
175
|
+
report.auth.expiry = credentials.expiry_date
|
|
176
|
+
? new Date(credentials.expiry_date).toISOString()
|
|
177
|
+
: 'unknown';
|
|
178
|
+
} catch (err) {
|
|
179
|
+
report.auth.status = 'expired_or_revoked';
|
|
180
|
+
report.auth.refreshError = err.message;
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
report.auth.status = report.auth.tokenFile === 'present' ? 'not_initialized' : 'missing';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- Service probes ---
|
|
187
|
+
if (client && report.auth.status === 'valid') {
|
|
188
|
+
// Drive
|
|
189
|
+
try {
|
|
190
|
+
const drive = google.drive({ version: 'v3', auth: client });
|
|
191
|
+
const res = await drive.about.get({ fields: 'user' });
|
|
192
|
+
report.services.drive = { status: 'ok', account: res.data.user?.emailAddress || 'unknown' };
|
|
193
|
+
} catch (err) {
|
|
194
|
+
report.services.drive = { status: 'error', error: err.message };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Gmail
|
|
198
|
+
try {
|
|
199
|
+
const gmail = google.gmail({ version: 'v1', auth: client });
|
|
200
|
+
const res = await gmail.users.getProfile({ userId: 'me' });
|
|
201
|
+
report.services.gmail = { status: 'ok', email: res.data.emailAddress || 'unknown' };
|
|
202
|
+
} catch (err) {
|
|
203
|
+
report.services.gmail = { status: 'error', error: err.message };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Calendar
|
|
207
|
+
try {
|
|
208
|
+
const calendar = google.calendar({ version: 'v3', auth: client });
|
|
209
|
+
await calendar.calendarList.list({ maxResults: 1 });
|
|
210
|
+
report.services.calendar = { status: 'ok' };
|
|
211
|
+
} catch (err) {
|
|
212
|
+
report.services.calendar = { status: 'error', error: err.message };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Forms — just check scope presence
|
|
216
|
+
report.services.forms = {
|
|
217
|
+
status: SCOPES.some(s => s.includes('forms')) ? 'configured' : 'no_scope',
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Docs/Sheets — covered by Drive auth
|
|
221
|
+
report.services.docs = { status: report.services.drive.status === 'ok' ? 'ok (via Drive auth)' : 'unknown' };
|
|
222
|
+
report.services.sheets = { status: report.services.drive.status === 'ok' ? 'ok (via Drive auth)' : 'unknown' };
|
|
223
|
+
} else {
|
|
224
|
+
report.services = { note: 'Skipped — auth not available' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --- Config summary ---
|
|
228
|
+
const configDir = getConfigDir();
|
|
229
|
+
report.config = {
|
|
230
|
+
configDir,
|
|
231
|
+
profile: process.env.GOOGLE_MCP_PROFILE || '(default)',
|
|
232
|
+
tokenPath,
|
|
233
|
+
credentialSource: process.env.GOOGLE_CLIENT_ID ? 'environment' : 'file',
|
|
234
|
+
scopes: SCOPES,
|
|
235
|
+
logFile: process.env.GOOGLE_MCP_LOG_FILE || '(not set)',
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// --- Recent logs ---
|
|
239
|
+
const logFilePath = process.env.GOOGLE_MCP_LOG_FILE === '1'
|
|
240
|
+
? path.join(configDir, 'server.log')
|
|
241
|
+
: process.env.GOOGLE_MCP_LOG_FILE;
|
|
242
|
+
if (logFilePath) {
|
|
243
|
+
try {
|
|
244
|
+
const logContent = await fs.readFile(logFilePath, 'utf8');
|
|
245
|
+
const lines = logContent.trimEnd().split('\n');
|
|
246
|
+
report.recentLogs = lines.slice(-20);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
report.recentLogs = err.code === 'ENOENT' ? '(log file not found)' : `(error reading log: ${err.message})`;
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
report.recentLogs = '(file logging not enabled — set GOOGLE_MCP_LOG_FILE to enable)';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// --- Environment ---
|
|
255
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
256
|
+
let pkgVersion = 'unknown';
|
|
257
|
+
try {
|
|
258
|
+
const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
|
|
259
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
260
|
+
pkgVersion = pkg.version;
|
|
261
|
+
} catch {}
|
|
262
|
+
report.environment = {
|
|
263
|
+
serverVersion: pkgVersion,
|
|
264
|
+
nodeVersion: process.version,
|
|
265
|
+
platform: process.platform,
|
|
266
|
+
osRelease: os.release(),
|
|
267
|
+
arch: process.arch,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return JSON.stringify(report, null, 2);
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// --- Feedback tool (always available) ---
|
|
275
|
+
server.addTool({
|
|
276
|
+
name: 'feedback',
|
|
277
|
+
description:
|
|
278
|
+
'Submit feedback or a bug report for google-tools-mcp. Automatically collects diagnostic info and generates a pre-filled GitHub issue URL.',
|
|
279
|
+
parameters: z.object({
|
|
280
|
+
type: z.enum(['bug', 'feature']).describe('Type of feedback'),
|
|
281
|
+
title: z.string().describe('Short summary'),
|
|
282
|
+
description: z.string().describe('Detailed description of the issue or feature request'),
|
|
283
|
+
}),
|
|
284
|
+
execute: async (args) => {
|
|
285
|
+
// Collect diagnostics
|
|
286
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
287
|
+
let pkgVersion = 'unknown';
|
|
288
|
+
try {
|
|
289
|
+
const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
|
|
290
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
291
|
+
pkgVersion = pkg.version;
|
|
292
|
+
} catch {}
|
|
293
|
+
|
|
294
|
+
let authStatus = 'unknown';
|
|
295
|
+
const tokenPath = getTokenPath();
|
|
296
|
+
try {
|
|
297
|
+
await fs.access(tokenPath);
|
|
298
|
+
const client = getAuthClientIfReady();
|
|
299
|
+
if (client) {
|
|
300
|
+
try {
|
|
301
|
+
await client.refreshAccessToken();
|
|
302
|
+
authStatus = 'valid';
|
|
303
|
+
} catch {
|
|
304
|
+
authStatus = 'expired_or_revoked';
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
authStatus = 'not_initialized';
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
authStatus = 'missing';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const enabledScopes = SCOPES.map(s => s.split('/').pop());
|
|
314
|
+
|
|
315
|
+
// Build markdown body
|
|
316
|
+
const diagnostics = [
|
|
317
|
+
`- **Server version:** ${pkgVersion}`,
|
|
318
|
+
`- **Node version:** ${process.version}`,
|
|
319
|
+
`- **OS:** ${process.platform} ${os.release()} (${process.arch})`,
|
|
320
|
+
`- **Auth status:** ${authStatus}`,
|
|
321
|
+
`- **Scopes:** ${enabledScopes.join(', ')}`,
|
|
322
|
+
].join('\n');
|
|
323
|
+
|
|
324
|
+
const body = [
|
|
325
|
+
`## Description`,
|
|
326
|
+
``,
|
|
327
|
+
args.description,
|
|
328
|
+
``,
|
|
329
|
+
`<details>`,
|
|
330
|
+
`<summary>Diagnostic Info</summary>`,
|
|
331
|
+
``,
|
|
332
|
+
diagnostics,
|
|
333
|
+
``,
|
|
334
|
+
`</details>`,
|
|
335
|
+
].join('\n');
|
|
336
|
+
|
|
337
|
+
// Build GitHub issue URL
|
|
338
|
+
const label = args.type === 'bug' ? 'bug' : 'enhancement';
|
|
339
|
+
const params = new URLSearchParams({
|
|
340
|
+
title: args.title,
|
|
341
|
+
body,
|
|
342
|
+
labels: label,
|
|
343
|
+
});
|
|
344
|
+
const url = `https://github.com/karthikcsq/google-tools-mcp/issues/new?${params.toString()}`;
|
|
345
|
+
|
|
346
|
+
return JSON.stringify({
|
|
347
|
+
url,
|
|
348
|
+
markdown: body,
|
|
349
|
+
note: url.length > 8000
|
|
350
|
+
? 'The URL may be too long for some browsers. Use the markdown body below to create the issue manually.'
|
|
351
|
+
: 'Open the URL to create a pre-filled GitHub issue.',
|
|
352
|
+
}, null, 2);
|
|
353
|
+
},
|
|
354
|
+
});
|
|
143
355
|
}
|