mcp-google-extras 1.0.0 → 1.0.2
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 +88 -25
- package/dist/clients.js +6 -2
- package/dist/index.js +2 -4
- package/dist/tools/drive/searchGoogleDocs.js +28 -3
- package/package.json +1 -1
package/dist/auth.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as fs from 'fs/promises';
|
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import * as os from 'os';
|
|
7
7
|
import * as http from 'http';
|
|
8
|
+
import { exec } from 'child_process';
|
|
8
9
|
import { fileURLToPath } from 'url';
|
|
9
10
|
import { logger } from './logger.js';
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
@@ -42,42 +43,83 @@ const SCOPES = [
|
|
|
42
43
|
'https://www.googleapis.com/auth/script.external_request',
|
|
43
44
|
];
|
|
44
45
|
// ---------------------------------------------------------------------------
|
|
46
|
+
// .env file loader
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
async function loadEnvFile(filePath) {
|
|
49
|
+
try {
|
|
50
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
51
|
+
for (const line of content.split('\n')) {
|
|
52
|
+
const trimmed = line.trim();
|
|
53
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
54
|
+
const eqIdx = trimmed.indexOf('=');
|
|
55
|
+
if (eqIdx === -1) continue;
|
|
56
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
57
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
58
|
+
// Strip surrounding quotes
|
|
59
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
60
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
61
|
+
value = value.slice(1, -1);
|
|
62
|
+
}
|
|
63
|
+
if (!process.env[key]) {
|
|
64
|
+
process.env[key] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
45
73
|
// Client secrets resolution
|
|
46
74
|
// ---------------------------------------------------------------------------
|
|
47
75
|
/**
|
|
48
76
|
* Resolves OAuth client ID and secret.
|
|
49
77
|
*
|
|
50
78
|
* Priority:
|
|
51
|
-
* 1. GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET env vars (
|
|
52
|
-
* 2.
|
|
79
|
+
* 1. GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET env vars (including from MCP config)
|
|
80
|
+
* 2. .env file in config dir (~/.config/google-docs-mcp/.env)
|
|
81
|
+
* 3. .env file in project root
|
|
82
|
+
* 4. credentials.json in config dir (~/.config/google-docs-mcp/credentials.json)
|
|
83
|
+
* 5. credentials.json in project root (legacy dev fallback)
|
|
53
84
|
*/
|
|
54
85
|
async function loadClientSecrets() {
|
|
55
|
-
// 1.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (envId && envSecret) {
|
|
59
|
-
return { client_id: envId, client_secret: envSecret };
|
|
86
|
+
// 1. Check env vars first (may already be set via MCP config)
|
|
87
|
+
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
|
88
|
+
return { client_id: process.env.GOOGLE_CLIENT_ID, client_secret: process.env.GOOGLE_CLIENT_SECRET };
|
|
60
89
|
}
|
|
61
|
-
// 2.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
client_id: key.client_id,
|
|
71
|
-
client_secret: key.client_secret,
|
|
72
|
-
};
|
|
90
|
+
// 2–3. Try loading .env files (config dir first, then project root)
|
|
91
|
+
const configDir = getConfigDir();
|
|
92
|
+
await loadEnvFile(path.join(configDir, '.env'));
|
|
93
|
+
await loadEnvFile(path.join(projectRootDir, '.env'));
|
|
94
|
+
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
|
95
|
+
logger.info('Loaded client credentials from .env file.');
|
|
96
|
+
return { client_id: process.env.GOOGLE_CLIENT_ID, client_secret: process.env.GOOGLE_CLIENT_SECRET };
|
|
73
97
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
98
|
+
// 4–5. Try credentials.json (config dir first, then project root)
|
|
99
|
+
const credentialsPaths = [
|
|
100
|
+
path.join(configDir, 'credentials.json'),
|
|
101
|
+
CREDENTIALS_PATH,
|
|
102
|
+
];
|
|
103
|
+
for (const credPath of credentialsPaths) {
|
|
104
|
+
try {
|
|
105
|
+
const content = await fs.readFile(credPath, 'utf8');
|
|
106
|
+
const keys = JSON.parse(content);
|
|
107
|
+
const key = keys.installed || keys.web;
|
|
108
|
+
if (key) {
|
|
109
|
+
logger.info('Loaded client credentials from', credPath);
|
|
110
|
+
return { client_id: key.client_id, client_secret: key.client_secret };
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
if (err.code !== 'ENOENT') throw err;
|
|
78
114
|
}
|
|
79
|
-
throw err;
|
|
80
115
|
}
|
|
116
|
+
const configDirDisplay = configDir.replace(os.homedir(), '~');
|
|
117
|
+
throw new Error(
|
|
118
|
+
'No OAuth credentials found. Provide them in any of these ways:\n' +
|
|
119
|
+
` 1. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars in your MCP config\n` +
|
|
120
|
+
` 2. Create ${configDirDisplay}/.env with GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET\n` +
|
|
121
|
+
` 3. Place your credentials.json (from Google Cloud Console) in ${configDirDisplay}/`
|
|
122
|
+
);
|
|
81
123
|
}
|
|
82
124
|
// ---------------------------------------------------------------------------
|
|
83
125
|
// Service account auth (unchanged)
|
|
@@ -144,6 +186,25 @@ async function saveCredentials(client) {
|
|
|
144
186
|
logger.info('Token stored to', tokenPath);
|
|
145
187
|
}
|
|
146
188
|
// ---------------------------------------------------------------------------
|
|
189
|
+
// Auto-open browser (cross-platform)
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
function openBrowser(url) {
|
|
192
|
+
const platform = process.platform;
|
|
193
|
+
let cmd;
|
|
194
|
+
if (platform === 'win32') {
|
|
195
|
+
cmd = `start "" "${url}"`;
|
|
196
|
+
} else if (platform === 'darwin') {
|
|
197
|
+
cmd = `open "${url}"`;
|
|
198
|
+
} else {
|
|
199
|
+
cmd = `xdg-open "${url}"`;
|
|
200
|
+
}
|
|
201
|
+
exec(cmd, (err) => {
|
|
202
|
+
if (err) {
|
|
203
|
+
logger.warn('Could not auto-open browser. Please open this URL manually.');
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
147
208
|
// Interactive OAuth browser flow
|
|
148
209
|
// ---------------------------------------------------------------------------
|
|
149
210
|
async function authenticate() {
|
|
@@ -158,7 +219,9 @@ async function authenticate() {
|
|
|
158
219
|
access_type: 'offline',
|
|
159
220
|
scope: SCOPES.join(' '),
|
|
160
221
|
});
|
|
161
|
-
logger.info('
|
|
222
|
+
logger.info('Opening browser for authorization...');
|
|
223
|
+
logger.info('If the browser does not open, visit this URL:', authorizeUrl);
|
|
224
|
+
openBrowser(authorizeUrl);
|
|
162
225
|
// Wait for the OAuth callback
|
|
163
226
|
const code = await new Promise((resolve, reject) => {
|
|
164
227
|
server.on('request', (req, res) => {
|
package/dist/clients.js
CHANGED
|
@@ -24,13 +24,17 @@ export async function initializeGoogleClient() {
|
|
|
24
24
|
logger.info('Google API client authorized successfully.');
|
|
25
25
|
}
|
|
26
26
|
catch (error) {
|
|
27
|
-
logger.error('
|
|
27
|
+
logger.error('Failed to initialize Google API client:', error);
|
|
28
28
|
authClient = null;
|
|
29
29
|
googleDocs = null;
|
|
30
30
|
googleDrive = null;
|
|
31
31
|
googleSheets = null;
|
|
32
32
|
googleScript = null;
|
|
33
|
-
throw new
|
|
33
|
+
throw new UserError(
|
|
34
|
+
'Google authentication required. A browser window should have opened automatically. ' +
|
|
35
|
+
'If not, run: npx mcp-google-extras auth\n\n' +
|
|
36
|
+
'Details: ' + (error.message || error)
|
|
37
|
+
);
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
if (authClient && !googleDocs) {
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
// mcp-google-extras auth Run the interactive OAuth flow
|
|
9
9
|
import { FastMCP } from 'fastmcp';
|
|
10
10
|
import { buildCachedToolsListPayload, collectToolsWhileRegistering, installCachedToolsListHandler, } from './cachedToolsList.js';
|
|
11
|
-
import { initializeGoogleClient } from './clients.js';
|
|
12
11
|
import { registerAllTools } from './tools/index.js';
|
|
13
12
|
import { logger } from './logger.js';
|
|
14
13
|
// --- Auth subcommand ---
|
|
@@ -40,13 +39,12 @@ const registeredTools = [];
|
|
|
40
39
|
collectToolsWhileRegistering(server, registeredTools);
|
|
41
40
|
registerAllTools(server);
|
|
42
41
|
try {
|
|
43
|
-
|
|
44
|
-
logger.info('Starting Ultimate Google Docs & Sheets MCP server...');
|
|
42
|
+
logger.info('Starting mcp-google-extras server...');
|
|
45
43
|
const cachedToolsList = await buildCachedToolsListPayload(registeredTools);
|
|
46
44
|
await server.start({ transportType: 'stdio' });
|
|
47
45
|
installCachedToolsListHandler(server, cachedToolsList);
|
|
48
46
|
logger.info('MCP Server running using stdio. Awaiting client connection...');
|
|
49
|
-
logger.info('
|
|
47
|
+
logger.info('Google auth will run automatically on first tool call.');
|
|
50
48
|
}
|
|
51
49
|
catch (startError) {
|
|
52
50
|
logger.error('FATAL: Server failed to start:', startError.message || startError);
|
|
@@ -4,7 +4,7 @@ import { getDriveClient } from '../../clients.js';
|
|
|
4
4
|
export function register(server) {
|
|
5
5
|
server.addTool({
|
|
6
6
|
name: 'searchDocuments',
|
|
7
|
-
description: 'Searches for documents by name, content, or both. Use listDocuments for browsing and this tool for targeted queries.',
|
|
7
|
+
description: 'Searches for documents by name, content, or both. Finds Google Docs, Word (.docx), and PDF files. Use listDocuments for browsing and this tool for targeted queries.',
|
|
8
8
|
parameters: z.object({
|
|
9
9
|
query: z.string().min(1).describe('Search term to find in document names or content.'),
|
|
10
10
|
searchIn: z
|
|
@@ -12,6 +12,11 @@ export function register(server) {
|
|
|
12
12
|
.optional()
|
|
13
13
|
.default('both')
|
|
14
14
|
.describe('Where to search: document names, content, or both.'),
|
|
15
|
+
fileType: z
|
|
16
|
+
.enum(['all', 'google-doc', 'docx', 'pdf'])
|
|
17
|
+
.optional()
|
|
18
|
+
.default('all')
|
|
19
|
+
.describe('Filter by file type: all document types, Google Docs only, Word (.docx) only, or PDF only.'),
|
|
15
20
|
maxResults: z
|
|
16
21
|
.number()
|
|
17
22
|
.int()
|
|
@@ -29,7 +34,21 @@ export function register(server) {
|
|
|
29
34
|
const drive = await getDriveClient();
|
|
30
35
|
log.info(`Searching Google Docs for: "${args.query}" in ${args.searchIn}`);
|
|
31
36
|
try {
|
|
32
|
-
|
|
37
|
+
const mimeTypes = {
|
|
38
|
+
'google-doc': ["mimeType='application/vnd.google-apps.document'"],
|
|
39
|
+
'docx': ["mimeType='application/vnd.openxmlformats-officedocument.wordprocessingml.document'"],
|
|
40
|
+
'pdf': ["mimeType='application/pdf'"],
|
|
41
|
+
'all': [
|
|
42
|
+
"mimeType='application/vnd.google-apps.document'",
|
|
43
|
+
"mimeType='application/vnd.openxmlformats-officedocument.wordprocessingml.document'",
|
|
44
|
+
"mimeType='application/pdf'",
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
const selectedMimes = mimeTypes[args.fileType || 'all'];
|
|
48
|
+
const mimeFilter = selectedMimes.length === 1
|
|
49
|
+
? selectedMimes[0]
|
|
50
|
+
: `(${selectedMimes.join(' or ')})`;
|
|
51
|
+
let queryString = `${mimeFilter} and trashed=false`;
|
|
33
52
|
// Add search criteria
|
|
34
53
|
if (args.searchIn === 'name') {
|
|
35
54
|
queryString += ` and name contains '${args.query}'`;
|
|
@@ -48,14 +67,20 @@ export function register(server) {
|
|
|
48
67
|
q: queryString,
|
|
49
68
|
pageSize: args.maxResults,
|
|
50
69
|
orderBy: 'modifiedTime desc',
|
|
51
|
-
fields: 'files(id,name,modifiedTime,createdTime,webViewLink,owners(displayName),parents)',
|
|
70
|
+
fields: 'files(id,name,mimeType,modifiedTime,createdTime,webViewLink,owners(displayName),parents)',
|
|
52
71
|
supportsAllDrives: true,
|
|
53
72
|
includeItemsFromAllDrives: true,
|
|
54
73
|
});
|
|
55
74
|
const files = response.data.files || [];
|
|
75
|
+
const mimeToType = {
|
|
76
|
+
'application/vnd.google-apps.document': 'google-doc',
|
|
77
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
|
|
78
|
+
'application/pdf': 'pdf',
|
|
79
|
+
};
|
|
56
80
|
const documents = files.map((file) => ({
|
|
57
81
|
id: file.id,
|
|
58
82
|
name: file.name,
|
|
83
|
+
type: mimeToType[file.mimeType] || file.mimeType,
|
|
59
84
|
modifiedTime: file.modifiedTime,
|
|
60
85
|
owner: file.owners?.[0]?.displayName || null,
|
|
61
86
|
url: file.webViewLink,
|