@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.
- package/README.md +18 -27
- package/dist/auth.js +267 -159
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
271
|
+
For running via `npx` or in containerized environments, pass OAuth credentials via environment variables.
|
|
272
272
|
|
|
273
|
-
**
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
|
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
|
|
6
|
+
import * as http from 'http';
|
|
7
|
+
import * as os from 'os';
|
|
7
8
|
import { fileURLToPath } from 'url';
|
|
8
|
-
|
|
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
|
|
13
|
-
const
|
|
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
|
-
// ---
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
// ---
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
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
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
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 (
|
|
99
|
-
|
|
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(
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
67
|
+
// Not found
|
|
120
68
|
}
|
|
69
|
+
return null;
|
|
121
70
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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.
|
|
183
|
-
// 3.
|
|
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.
|
|
272
|
+
console.error('Service account path detected. Using service account authentication...');
|
|
188
273
|
return authorizeWithServiceAccount();
|
|
189
274
|
}
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
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": {
|