google-tools-mcp 1.0.14 → 1.0.16

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/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) {
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;
@@ -7,6 +7,7 @@ export function register(server) {
7
7
  name: 'get_events',
8
8
  description:
9
9
  'Retrieves events from a Google Calendar. Can get a single event by ID, or list events in a time range with optional search. ' +
10
+ 'By default only returns future events (time_min defaults to now). To retrieve past events, set time_min to a past date (e.g. "2025-01-01T00:00:00Z"). ' +
10
11
  'Use calendar_id to query another user\'s calendar (requires sharing) or "primary" for the authenticated user.',
11
12
  parameters: z.object({
12
13
  calendar_id: z
@@ -8,7 +8,9 @@ export function register(server) {
8
8
  description:
9
9
  'Lists individual occurrences of a recurring event. Use this to find specific instances ' +
10
10
  'you want to modify or cancel (e.g. "cancel next Tuesday\'s standup"). ' +
11
- 'Each instance has its own event ID that you can pass to manage_event.',
11
+ 'Each instance has its own event ID that you can pass to manage_event. ' +
12
+ 'By default only returns future instances (time_min defaults to now). To retrieve past instances, set time_min to a past date. ' +
13
+ 'Note: all parameters use snake_case (event_id, time_min, time_max), not camelCase.',
12
14
  parameters: z.object({
13
15
  calendar_id: z
14
16
  .string()
@@ -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 { getTokenPath } from '../auth.js';
9
- import { resetClients, withAuthRetry } from '../clients.js';
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "google-tools-mcp",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Combined Google Workspace MCP server (Drive, Docs, Sheets, Gmail) with lazy-loaded tool categories",
5
5
  "type": "module",
6
6
  "bin": {