@brainwavesio/google-docs-mcp 1.0.0 → 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 +313 -0
- package/dist/googleDocsApiHelpers.js +617 -0
- package/dist/googleSheetsApiHelpers.js +356 -0
- package/dist/server.js +2215 -0
- package/dist/types.js +107 -0
- package/package.json +2 -1
- package/.repomix/bundles.json +0 -3
- package/SAMPLE_TASKS.md +0 -230
- package/assets/google.docs.mcp.1.gif +0 -0
- package/claude.md +0 -46
- package/docs/index.html +0 -228
- package/google docs mcp.mp4 +0 -0
- package/pages/pages.md +0 -0
- package/repomix-output.txt.xml +0 -4447
- package/src/auth.ts +0 -228
- package/src/backup/auth.ts.bak +0 -101
- package/src/backup/server.ts.bak +0 -481
- package/src/googleDocsApiHelpers.ts +0 -710
- package/src/googleSheetsApiHelpers.ts +0 -427
- package/src/server.ts +0 -2494
- package/src/types.ts +0 -136
- package/tests/helpers.test.js +0 -164
- package/tests/types.test.js +0 -69
- package/tsconfig.json +0 -17
- package/vscode.md +0 -168
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
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// src/auth.ts
|
|
2
|
+
import { google } from 'googleapis';
|
|
3
|
+
import { JWT } from 'google-auth-library';
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as http from 'http';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
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) ---
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
const projectRootDir = path.resolve(__dirname, '..');
|
|
17
|
+
const LEGACY_TOKEN_PATH = path.join(projectRootDir, 'token.json');
|
|
18
|
+
const LEGACY_CREDENTIALS_PATH = path.join(projectRootDir, 'credentials.json');
|
|
19
|
+
const SCOPES = [
|
|
20
|
+
'https://www.googleapis.com/auth/documents',
|
|
21
|
+
'https://www.googleapis.com/auth/drive',
|
|
22
|
+
'https://www.googleapis.com/auth/spreadsheets'
|
|
23
|
+
];
|
|
24
|
+
// --- Ensure config directory exists ---
|
|
25
|
+
async function ensureConfigDir() {
|
|
26
|
+
try {
|
|
27
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
// Directory might already exist
|
|
31
|
+
}
|
|
32
|
+
}
|
|
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
|
|
48
|
+
try {
|
|
49
|
+
const content = await fs.readFile(TOKEN_PATH, 'utf8');
|
|
50
|
+
const token = JSON.parse(content);
|
|
51
|
+
if (token.refresh_token) {
|
|
52
|
+
return token;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// Not found in config dir, try legacy location
|
|
57
|
+
}
|
|
58
|
+
// Try legacy token.json in project root
|
|
59
|
+
try {
|
|
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
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
// Not found
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
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
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
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');
|
|
219
|
+
}
|
|
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
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
// File not found or invalid
|
|
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;
|
|
261
|
+
}
|
|
262
|
+
// --- Main exported function ---
|
|
263
|
+
// Priority order:
|
|
264
|
+
// 1. Service account (SERVICE_ACCOUNT_PATH)
|
|
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
|
|
269
|
+
export async function authorize() {
|
|
270
|
+
// 1. Check for service account
|
|
271
|
+
if (process.env.SERVICE_ACCOUNT_PATH) {
|
|
272
|
+
console.error('Service account path detected. Using service account authentication...');
|
|
273
|
+
return authorizeWithServiceAccount();
|
|
274
|
+
}
|
|
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;
|
|
287
|
+
}
|
|
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.');
|
|
299
|
+
return client;
|
|
300
|
+
}
|
|
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.');
|
|
313
|
+
}
|