@brainwavesio/google-docs-mcp 1.0.1 → 1.1.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.
Files changed (3) hide show
  1. package/README.md +18 -27
  2. package/dist/auth.js +267 -159
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -8,13 +8,12 @@ Connect Claude Desktop (or other MCP clients) to your Google Docs, Google Sheets
8
8
 
9
9
  ## Quick Start (npx)
10
10
 
11
- If you have Google OAuth credentials, you can run this server without cloning:
11
+ **1. Get Google OAuth credentials** from [Google Cloud Console](https://console.cloud.google.com/):
12
+ - Create a project and enable Google Docs, Sheets, and Drive APIs
13
+ - Create OAuth 2.0 credentials (Desktop app type)
14
+ - Download the client ID and secret
12
15
 
13
- ```bash
14
- npx @brainwavesio/google-docs-mcp
15
- ```
16
-
17
- Or configure in Claude Desktop's `mcp_config.json`:
16
+ **2. Configure Claude Desktop** (`mcp_config.json` or `.mcp.json`):
18
17
 
19
18
  ```json
20
19
  {
@@ -24,15 +23,16 @@ Or configure in Claude Desktop's `mcp_config.json`:
24
23
  "args": ["@brainwavesio/google-docs-mcp"],
25
24
  "env": {
26
25
  "GOOGLE_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
27
- "GOOGLE_CLIENT_SECRET": "your-client-secret",
28
- "GOOGLE_REFRESH_TOKEN": "your-refresh-token"
26
+ "GOOGLE_CLIENT_SECRET": "your-client-secret"
29
27
  }
30
28
  }
31
29
  }
32
30
  }
33
31
  ```
34
32
 
35
- See [Environment Variable Authentication](#alternative-environment-variable-authentication-npx--containers) for details on getting a refresh token.
33
+ **3. First run:** The server will automatically open your browser for Google authorization. After you approve, the token is saved to `~/.config/google-docs-mcp/token.json` for future use.
34
+
35
+ That's it! No manual token copying required.
36
36
 
37
37
  ---
38
38
 
@@ -268,36 +268,28 @@ When `GOOGLE_IMPERSONATE_USER` is set, the server will impersonate that user whe
268
268
 
269
269
  ### Alternative: Environment Variable Authentication (npx / Containers)
270
270
 
271
- For running via `npx` or in containerized environments where you can't rely on local files, you can pass OAuth credentials entirely via environment variables.
271
+ For running via `npx` or in containerized environments, pass OAuth credentials via environment variables.
272
272
 
273
- **Option A: Individual environment variables**
273
+ **Required environment variables:**
274
274
 
275
275
  ```bash
276
- # Your OAuth client credentials (from credentials.json)
277
276
  export GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
278
277
  export GOOGLE_CLIENT_SECRET="your-client-secret"
279
- export GOOGLE_REFRESH_TOKEN="your-refresh-token"
280
278
  ```
281
279
 
282
- **Option B: Full credentials JSON as env var**
280
+ **Optional:** If you already have a refresh token, you can skip the browser auth:
283
281
 
284
282
  ```bash
285
- # The entire contents of credentials.json as a single env var
286
- export GOOGLE_CREDENTIALS_JSON='{"installed":{"client_id":"...","client_secret":"..."}}'
287
283
  export GOOGLE_REFRESH_TOKEN="your-refresh-token"
288
284
  ```
289
285
 
290
- **Getting a refresh token:**
291
-
292
- If you have the client ID and secret but no refresh token yet, run the server with just those set:
293
-
294
- ```bash
295
- GOOGLE_CLIENT_ID="..." GOOGLE_CLIENT_SECRET="..." npx @brainwavesio/google-docs-mcp
296
- ```
286
+ **How authentication works:**
297
287
 
298
- The server will prompt you through the OAuth flow and print the refresh token for you to save.
288
+ 1. On first run, if no saved token exists, the server opens your browser for Google authorization
289
+ 2. After you approve, the token is saved to `~/.config/google-docs-mcp/token.json`
290
+ 3. Subsequent runs use the saved token automatically
299
291
 
300
- **Claude Desktop config with env vars:**
292
+ **Claude Desktop config:**
301
293
 
302
294
  ```json
303
295
  {
@@ -307,8 +299,7 @@ The server will prompt you through the OAuth flow and print the refresh token fo
307
299
  "args": ["@brainwavesio/google-docs-mcp"],
308
300
  "env": {
309
301
  "GOOGLE_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
310
- "GOOGLE_CLIENT_SECRET": "your-client-secret",
311
- "GOOGLE_REFRESH_TOKEN": "your-refresh-token"
302
+ "GOOGLE_CLIENT_SECRET": "your-client-secret"
312
303
  }
313
304
  }
314
305
  }
package/dist/auth.js CHANGED
@@ -3,203 +3,311 @@ import { google } from 'googleapis';
3
3
  import { JWT } from 'google-auth-library';
4
4
  import * as fs from 'fs/promises';
5
5
  import * as path from 'path';
6
- import * as readline from 'readline/promises';
6
+ import * as http from 'http';
7
+ import * as os from 'os';
7
8
  import { fileURLToPath } from 'url';
8
- // --- Calculate paths relative to this script file (ESM way) ---
9
+ import open from 'open';
10
+ // --- Config directory for persistent token storage ---
11
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'google-docs-mcp');
12
+ const TOKEN_PATH = path.join(CONFIG_DIR, 'token.json');
13
+ // --- Legacy paths (for backwards compatibility) ---
9
14
  const __filename = fileURLToPath(import.meta.url);
10
15
  const __dirname = path.dirname(__filename);
11
16
  const projectRootDir = path.resolve(__dirname, '..');
12
- const TOKEN_PATH = path.join(projectRootDir, 'token.json');
13
- const CREDENTIALS_PATH = path.join(projectRootDir, 'credentials.json');
14
- // --- End of path calculation ---
17
+ const LEGACY_TOKEN_PATH = path.join(projectRootDir, 'token.json');
18
+ const LEGACY_CREDENTIALS_PATH = path.join(projectRootDir, 'credentials.json');
15
19
  const SCOPES = [
16
20
  'https://www.googleapis.com/auth/documents',
17
21
  'https://www.googleapis.com/auth/drive',
18
22
  'https://www.googleapis.com/auth/spreadsheets'
19
23
  ];
20
- // --- Environment variable authentication ---
21
- // Supports OAuth credentials via env vars for containerized/npx usage:
22
- // - GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN
23
- // - Or GOOGLE_CREDENTIALS_JSON (full credentials.json content as string)
24
- function hasEnvCredentials() {
25
- return !!((process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET && process.env.GOOGLE_REFRESH_TOKEN) ||
26
- process.env.GOOGLE_CREDENTIALS_JSON);
27
- }
28
- async function authorizeWithEnvCredentials() {
29
- let clientId;
30
- let clientSecret;
31
- let refreshToken;
32
- if (process.env.GOOGLE_CREDENTIALS_JSON) {
33
- // Parse the full credentials.json from env var
34
- const keys = JSON.parse(process.env.GOOGLE_CREDENTIALS_JSON);
35
- const key = keys.installed || keys.web;
36
- if (!key) {
37
- throw new Error('GOOGLE_CREDENTIALS_JSON must contain "installed" or "web" key');
38
- }
39
- clientId = key.client_id;
40
- clientSecret = key.client_secret;
41
- refreshToken = process.env.GOOGLE_REFRESH_TOKEN;
24
+ // --- Ensure config directory exists ---
25
+ async function ensureConfigDir() {
26
+ try {
27
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
42
28
  }
43
- else {
44
- clientId = process.env.GOOGLE_CLIENT_ID;
45
- clientSecret = process.env.GOOGLE_CLIENT_SECRET;
46
- refreshToken = process.env.GOOGLE_REFRESH_TOKEN;
47
- }
48
- const client = new google.auth.OAuth2(clientId, clientSecret);
49
- if (refreshToken) {
50
- client.setCredentials({ refresh_token: refreshToken });
51
- console.error('Using OAuth credentials from environment variables.');
52
- return client;
29
+ catch (err) {
30
+ // Directory might already exist
53
31
  }
54
- // No refresh token - need to do interactive auth flow
55
- console.error('No GOOGLE_REFRESH_TOKEN found. Starting interactive OAuth flow...');
56
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
57
- const authorizeUrl = client.generateAuthUrl({
58
- access_type: 'offline',
59
- scope: SCOPES.join(' '),
60
- });
61
- console.error('Authorize this app by visiting this url:', authorizeUrl);
62
- const code = await rl.question('Enter the code from that page here: ');
63
- rl.close();
64
- const { tokens } = await client.getToken(code);
65
- client.setCredentials(tokens);
66
- if (tokens.refresh_token) {
67
- console.error('\n=== SAVE THIS REFRESH TOKEN ===');
68
- console.error('Add this to your environment:');
69
- console.error(`GOOGLE_REFRESH_TOKEN=${tokens.refresh_token}`);
70
- console.error('===============================\n');
71
- }
72
- console.error('Authentication successful!');
73
- return client;
74
32
  }
75
- // --- End of environment variable authentication ---
76
- // --- Service Account Authentication ---
77
- async function authorizeWithServiceAccount() {
78
- const serviceAccountPath = process.env.SERVICE_ACCOUNT_PATH;
79
- const impersonateUser = process.env.GOOGLE_IMPERSONATE_USER;
33
+ // --- Save token to config directory ---
34
+ async function saveToken(credentials) {
35
+ await ensureConfigDir();
36
+ const payload = JSON.stringify({
37
+ type: 'authorized_user',
38
+ client_id: credentials.client_id,
39
+ client_secret: credentials.client_secret,
40
+ refresh_token: credentials.refresh_token,
41
+ }, null, 2);
42
+ await fs.writeFile(TOKEN_PATH, payload);
43
+ console.error(`Token saved to ${TOKEN_PATH}`);
44
+ }
45
+ // --- Load token from config directory or legacy location ---
46
+ async function loadSavedToken() {
47
+ // Try new config location first
80
48
  try {
81
- const keyFileContent = await fs.readFile(serviceAccountPath, 'utf8');
82
- const serviceAccountKey = JSON.parse(keyFileContent);
83
- const auth = new JWT({
84
- email: serviceAccountKey.client_email,
85
- key: serviceAccountKey.private_key,
86
- scopes: SCOPES,
87
- subject: impersonateUser,
88
- });
89
- await auth.authorize();
90
- if (impersonateUser) {
91
- console.error(`Service Account authentication successful, impersonating: ${impersonateUser}`);
92
- }
93
- else {
94
- console.error('Service Account authentication successful!');
49
+ const content = await fs.readFile(TOKEN_PATH, 'utf8');
50
+ const token = JSON.parse(content);
51
+ if (token.refresh_token) {
52
+ return token;
95
53
  }
96
- return auth;
97
54
  }
98
- catch (error) {
99
- if (error.code === 'ENOENT') {
100
- console.error(`FATAL: Service account key file not found at path: ${serviceAccountPath}`);
101
- throw new Error(`Service account key file not found. Please check the path in SERVICE_ACCOUNT_PATH.`);
102
- }
103
- console.error('FATAL: Error loading or authorizing the service account key:', error.message);
104
- throw new Error('Failed to authorize using the service account. Ensure the key file is valid and the path is correct.');
55
+ catch (err) {
56
+ // Not found in config dir, try legacy location
105
57
  }
106
- }
107
- // --- End of Service Account Authentication ---
108
- // --- File-based OAuth (original behavior) ---
109
- async function loadSavedCredentialsIfExist() {
58
+ // Try legacy token.json in project root
110
59
  try {
111
- const content = await fs.readFile(TOKEN_PATH);
112
- const credentials = JSON.parse(content.toString());
113
- const { client_secret, client_id, redirect_uris } = await loadClientSecrets();
114
- const client = new google.auth.OAuth2(client_id, client_secret, redirect_uris?.[0]);
115
- client.setCredentials(credentials);
116
- return client;
60
+ const content = await fs.readFile(LEGACY_TOKEN_PATH, 'utf8');
61
+ const token = JSON.parse(content);
62
+ if (token.refresh_token) {
63
+ return token;
64
+ }
117
65
  }
118
66
  catch (err) {
119
- return null;
67
+ // Not found
120
68
  }
69
+ return null;
121
70
  }
122
- async function loadClientSecrets() {
123
- const content = await fs.readFile(CREDENTIALS_PATH);
124
- const keys = JSON.parse(content.toString());
125
- const key = keys.installed || keys.web;
126
- if (!key)
127
- throw new Error("Could not find client secrets in credentials.json.");
128
- return {
129
- client_id: key.client_id,
130
- client_secret: key.client_secret,
131
- redirect_uris: key.redirect_uris || ['http://localhost:3000/'],
132
- client_type: keys.web ? 'web' : 'installed'
133
- };
134
- }
135
- async function saveCredentials(client) {
136
- const { client_secret, client_id } = await loadClientSecrets();
137
- const payload = JSON.stringify({
138
- type: 'authorized_user',
139
- client_id: client_id,
140
- client_secret: client_secret,
141
- refresh_token: client.credentials.refresh_token,
71
+ // --- Browser-based OAuth flow ---
72
+ async function authenticateWithBrowser(clientId, clientSecret) {
73
+ return new Promise((resolve, reject) => {
74
+ // Find an available port
75
+ const server = http.createServer();
76
+ server.listen(0, '127.0.0.1', async () => {
77
+ const address = server.address();
78
+ if (!address || typeof address === 'string') {
79
+ reject(new Error('Failed to start local server'));
80
+ return;
81
+ }
82
+ const port = address.port;
83
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
84
+ const client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
85
+ const authorizeUrl = client.generateAuthUrl({
86
+ access_type: 'offline',
87
+ scope: SCOPES,
88
+ prompt: 'consent', // Force consent to ensure we get a refresh token
89
+ });
90
+ console.error('\n========================================');
91
+ console.error('Opening browser for Google authorization...');
92
+ console.error('If the browser does not open, visit this URL:');
93
+ console.error(authorizeUrl);
94
+ console.error('========================================\n');
95
+ // Handle the OAuth callback
96
+ server.on('request', async (req, res) => {
97
+ if (!req.url?.startsWith('/callback')) {
98
+ res.writeHead(404);
99
+ res.end('Not found');
100
+ return;
101
+ }
102
+ const url = new URL(req.url, `http://127.0.0.1:${port}`);
103
+ const code = url.searchParams.get('code');
104
+ const error = url.searchParams.get('error');
105
+ if (error) {
106
+ res.writeHead(400, { 'Content-Type': 'text/html' });
107
+ res.end(`
108
+ <html>
109
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
110
+ <h1>Authorization Failed</h1>
111
+ <p>Error: ${error}</p>
112
+ <p>You can close this window.</p>
113
+ </body>
114
+ </html>
115
+ `);
116
+ server.close();
117
+ reject(new Error(`Authorization failed: ${error}`));
118
+ return;
119
+ }
120
+ if (!code) {
121
+ res.writeHead(400, { 'Content-Type': 'text/html' });
122
+ res.end(`
123
+ <html>
124
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
125
+ <h1>Authorization Failed</h1>
126
+ <p>No authorization code received.</p>
127
+ <p>You can close this window.</p>
128
+ </body>
129
+ </html>
130
+ `);
131
+ server.close();
132
+ reject(new Error('No authorization code received'));
133
+ return;
134
+ }
135
+ try {
136
+ const { tokens } = await client.getToken(code);
137
+ client.setCredentials(tokens);
138
+ if (tokens.refresh_token) {
139
+ await saveToken({
140
+ client_id: clientId,
141
+ client_secret: clientSecret,
142
+ refresh_token: tokens.refresh_token,
143
+ });
144
+ }
145
+ res.writeHead(200, { 'Content-Type': 'text/html' });
146
+ res.end(`
147
+ <html>
148
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
149
+ <h1>Authorization Successful!</h1>
150
+ <p>You can close this window and return to your application.</p>
151
+ <script>window.close();</script>
152
+ </body>
153
+ </html>
154
+ `);
155
+ server.close();
156
+ console.error('Authorization successful!');
157
+ resolve(client);
158
+ }
159
+ catch (err) {
160
+ res.writeHead(500, { 'Content-Type': 'text/html' });
161
+ res.end(`
162
+ <html>
163
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
164
+ <h1>Authorization Failed</h1>
165
+ <p>Failed to exchange code for tokens.</p>
166
+ <p>You can close this window.</p>
167
+ </body>
168
+ </html>
169
+ `);
170
+ server.close();
171
+ reject(err);
172
+ }
173
+ });
174
+ // Set a timeout for the auth flow
175
+ const timeout = setTimeout(() => {
176
+ server.close();
177
+ reject(new Error('Authorization timed out after 5 minutes'));
178
+ }, 5 * 60 * 1000);
179
+ server.on('close', () => {
180
+ clearTimeout(timeout);
181
+ });
182
+ // Open the browser
183
+ try {
184
+ await open(authorizeUrl);
185
+ }
186
+ catch (err) {
187
+ console.error('Failed to open browser automatically.');
188
+ console.error('Please open the URL above manually.');
189
+ }
190
+ });
191
+ server.on('error', (err) => {
192
+ reject(err);
193
+ });
142
194
  });
143
- await fs.writeFile(TOKEN_PATH, payload);
144
- console.error('Token stored to', TOKEN_PATH);
145
195
  }
146
- async function authenticate() {
147
- const { client_secret, client_id, redirect_uris, client_type } = await loadClientSecrets();
148
- const redirectUri = client_type === 'web' ? redirect_uris[0] : 'urn:ietf:wg:oauth:2.0:oob';
149
- console.error(`DEBUG: Using redirect URI: ${redirectUri}`);
150
- console.error(`DEBUG: Client type: ${client_type}`);
151
- const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
152
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
153
- const authorizeUrl = oAuth2Client.generateAuthUrl({
154
- access_type: 'offline',
155
- scope: SCOPES.join(' '),
156
- });
157
- console.error('DEBUG: Generated auth URL:', authorizeUrl);
158
- console.error('Authorize this app by visiting this url:', authorizeUrl);
159
- const code = await rl.question('Enter the code from that page here: ');
160
- rl.close();
161
- try {
162
- const { tokens } = await oAuth2Client.getToken(code);
163
- oAuth2Client.setCredentials(tokens);
164
- if (tokens.refresh_token) {
165
- await saveCredentials(oAuth2Client);
196
+ // --- Get OAuth credentials from env vars or files ---
197
+ function getClientCredentials() {
198
+ // Check environment variables first
199
+ if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
200
+ return {
201
+ clientId: process.env.GOOGLE_CLIENT_ID,
202
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
203
+ };
204
+ }
205
+ // Check for GOOGLE_CREDENTIALS_JSON env var
206
+ if (process.env.GOOGLE_CREDENTIALS_JSON) {
207
+ try {
208
+ const keys = JSON.parse(process.env.GOOGLE_CREDENTIALS_JSON);
209
+ const key = keys.installed || keys.web;
210
+ if (key) {
211
+ return {
212
+ clientId: key.client_id,
213
+ clientSecret: key.client_secret,
214
+ };
215
+ }
216
+ }
217
+ catch (err) {
218
+ console.error('Failed to parse GOOGLE_CREDENTIALS_JSON');
166
219
  }
167
- else {
168
- console.error("Did not receive refresh token. Token might expire.");
220
+ }
221
+ return null;
222
+ }
223
+ // --- Load credentials.json file (legacy support) ---
224
+ async function loadCredentialsFile() {
225
+ try {
226
+ const content = await fs.readFile(LEGACY_CREDENTIALS_PATH, 'utf8');
227
+ const keys = JSON.parse(content);
228
+ const key = keys.installed || keys.web;
229
+ if (key) {
230
+ return {
231
+ clientId: key.client_id,
232
+ clientSecret: key.client_secret,
233
+ };
169
234
  }
170
- console.error('Authentication successful!');
171
- return oAuth2Client;
172
235
  }
173
236
  catch (err) {
174
- console.error('Error retrieving access token', err);
175
- throw new Error('Authentication failed');
237
+ // File not found or invalid
176
238
  }
239
+ return null;
240
+ }
241
+ // --- Service Account Authentication ---
242
+ async function authorizeWithServiceAccount() {
243
+ const serviceAccountPath = process.env.SERVICE_ACCOUNT_PATH;
244
+ const impersonateUser = process.env.GOOGLE_IMPERSONATE_USER;
245
+ const keyFileContent = await fs.readFile(serviceAccountPath, 'utf8');
246
+ const serviceAccountKey = JSON.parse(keyFileContent);
247
+ const auth = new JWT({
248
+ email: serviceAccountKey.client_email,
249
+ key: serviceAccountKey.private_key,
250
+ scopes: SCOPES,
251
+ subject: impersonateUser,
252
+ });
253
+ await auth.authorize();
254
+ if (impersonateUser) {
255
+ console.error(`Service Account authentication successful, impersonating: ${impersonateUser}`);
256
+ }
257
+ else {
258
+ console.error('Service Account authentication successful!');
259
+ }
260
+ return auth;
177
261
  }
178
- // --- End of file-based OAuth ---
179
262
  // --- Main exported function ---
180
263
  // Priority order:
181
264
  // 1. Service account (SERVICE_ACCOUNT_PATH)
182
- // 2. Environment variables (GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET + GOOGLE_REFRESH_TOKEN, or GOOGLE_CREDENTIALS_JSON)
183
- // 3. File-based OAuth (credentials.json + token.json)
265
+ // 2. Saved token + env credentials
266
+ // 3. Env var refresh token (GOOGLE_REFRESH_TOKEN)
267
+ // 4. Browser-based OAuth flow
268
+ // 5. Legacy file-based credentials
184
269
  export async function authorize() {
185
270
  // 1. Check for service account
186
271
  if (process.env.SERVICE_ACCOUNT_PATH) {
187
- console.error('Service account path detected. Attempting service account authentication...');
272
+ console.error('Service account path detected. Using service account authentication...');
188
273
  return authorizeWithServiceAccount();
189
274
  }
190
- // 2. Check for env var credentials
191
- if (hasEnvCredentials()) {
192
- console.error('Environment variable credentials detected. Using env-based authentication...');
193
- return authorizeWithEnvCredentials();
275
+ // Get client credentials from env or files
276
+ let credentials = getClientCredentials();
277
+ // 2. Check for saved token
278
+ const savedToken = await loadSavedToken();
279
+ if (savedToken) {
280
+ // Use saved token - prefer env credentials if available, otherwise use token's credentials
281
+ const clientId = credentials?.clientId || savedToken.client_id;
282
+ const clientSecret = credentials?.clientSecret || savedToken.client_secret;
283
+ const client = new google.auth.OAuth2(clientId, clientSecret);
284
+ client.setCredentials({ refresh_token: savedToken.refresh_token });
285
+ console.error('Using saved credentials from ~/.config/google-docs-mcp/');
286
+ return client;
194
287
  }
195
- // 3. Fall back to file-based OAuth
196
- console.error('Using file-based OAuth flow...');
197
- let client = await loadSavedCredentialsIfExist();
198
- if (client) {
199
- console.error('Using saved credentials.');
288
+ // 3. Check for refresh token in env var
289
+ if (credentials && process.env.GOOGLE_REFRESH_TOKEN) {
290
+ const client = new google.auth.OAuth2(credentials.clientId, credentials.clientSecret);
291
+ client.setCredentials({ refresh_token: process.env.GOOGLE_REFRESH_TOKEN });
292
+ // Save this token for future use
293
+ await saveToken({
294
+ client_id: credentials.clientId,
295
+ client_secret: credentials.clientSecret,
296
+ refresh_token: process.env.GOOGLE_REFRESH_TOKEN,
297
+ });
298
+ console.error('Using refresh token from environment variable.');
200
299
  return client;
201
300
  }
202
- console.error('Starting authentication flow...');
203
- client = await authenticate();
204
- return client;
301
+ // 4. Try to load credentials from file if not in env
302
+ if (!credentials) {
303
+ credentials = await loadCredentialsFile();
304
+ }
305
+ // 5. If we have client credentials, do browser-based OAuth
306
+ if (credentials) {
307
+ console.error('No saved token found. Starting browser-based authentication...');
308
+ return authenticateWithBrowser(credentials.clientId, credentials.clientSecret);
309
+ }
310
+ // No credentials available
311
+ throw new Error('No Google credentials found. Please set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables, ' +
312
+ 'or place a credentials.json file in the project directory.');
205
313
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainwavesio/google-docs-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -30,6 +30,7 @@
30
30
  "fastmcp": "^3.24.0",
31
31
  "google-auth-library": "^9.15.1",
32
32
  "googleapis": "^148.0.0",
33
+ "open": "^10.1.0",
33
34
  "zod": "^3.24.2"
34
35
  },
35
36
  "devDependencies": {