@piotr-agier/google-drive-mcp 1.7.0 → 1.7.1
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 +1 -5
- package/dist/index.js +55 -98
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -400,14 +400,10 @@ Add the server to your Claude Desktop configuration:
|
|
|
400
400
|
- `revisionId`: Revision ID to restore
|
|
401
401
|
- `confirm`: Must be `true` to execute restore
|
|
402
402
|
|
|
403
|
-
#### Auth Diagnostics
|
|
403
|
+
#### Auth Diagnostics (v1.7.0)
|
|
404
404
|
- **authGetStatus** - Show token/scopes/auth health diagnostics (machine + human readable)
|
|
405
405
|
- **authListScopes** - Show configured/requested scopes, granted scopes, missing scopes, and presets
|
|
406
406
|
- **authTestFileAccess** - Test Drive access (optionally against a specific `fileId`)
|
|
407
|
-
- **authClearTokens** - Clear saved OAuth tokens (`confirm=true` required)
|
|
408
|
-
- **authSuggestScopePreset** - Get scope configuration instructions for a preset
|
|
409
|
-
- `preset`: `readonly` | `content-editor` | `full`
|
|
410
|
-
- `clearTokens`: optional best-effort token clear
|
|
411
407
|
|
|
412
408
|
- **uploadFile** - Upload a local file (any type: image, audio, video, PDF, etc.) to Google Drive
|
|
413
409
|
- `localPath`: Absolute path to the local file
|
package/dist/index.js
CHANGED
|
@@ -699,7 +699,7 @@ __export(drive_exports, {
|
|
|
699
699
|
});
|
|
700
700
|
import { z } from "zod";
|
|
701
701
|
import { existsSync as existsSync2, statSync as statSync2, createReadStream } from "fs";
|
|
702
|
-
import { mkdtemp, readFile as readFile3, writeFile as writeFile2, rm
|
|
702
|
+
import { mkdtemp, readFile as readFile3, writeFile as writeFile2, rm } from "fs/promises";
|
|
703
703
|
import { tmpdir } from "os";
|
|
704
704
|
import { basename as basename2, extname as extname2, join as join3 } from "path";
|
|
705
705
|
import { PDFDocument } from "pdf-lib";
|
|
@@ -916,7 +916,8 @@ var BINARY_MIME_TYPES = {
|
|
|
916
916
|
var SearchSchema = z.object({
|
|
917
917
|
query: z.string().min(1, "Search query is required"),
|
|
918
918
|
pageSize: z.number().int().min(1).max(100).optional(),
|
|
919
|
-
pageToken: z.string().optional()
|
|
919
|
+
pageToken: z.string().optional(),
|
|
920
|
+
rawQuery: z.boolean().optional()
|
|
920
921
|
});
|
|
921
922
|
var CreateTextFileSchema = z.object({
|
|
922
923
|
name: z.string().min(1, "File name is required"),
|
|
@@ -1050,13 +1051,6 @@ var RestoreRevisionSchema = z.object({
|
|
|
1050
1051
|
var AuthTestFileAccessSchema = z.object({
|
|
1051
1052
|
fileId: z.string().optional()
|
|
1052
1053
|
});
|
|
1053
|
-
var AuthClearTokensSchema = z.object({
|
|
1054
|
-
confirm: z.boolean().optional().default(false)
|
|
1055
|
-
});
|
|
1056
|
-
var AuthSetScopePresetSchema = z.object({
|
|
1057
|
-
preset: z.enum(["readonly", "content-editor", "full"]),
|
|
1058
|
-
clearTokens: z.boolean().optional().default(false)
|
|
1059
|
-
});
|
|
1060
1054
|
function getGrantedScopesFromAuthClient(ctx) {
|
|
1061
1055
|
const scopeRaw = ctx.authClient?.credentials?.scope;
|
|
1062
1056
|
if (!scopeRaw || typeof scopeRaw !== "string") return [];
|
|
@@ -1065,13 +1059,14 @@ function getGrantedScopesFromAuthClient(ctx) {
|
|
|
1065
1059
|
var toolDefinitions = [
|
|
1066
1060
|
{
|
|
1067
1061
|
name: "search",
|
|
1068
|
-
description: "Search for files in Google Drive",
|
|
1062
|
+
description: "Search for files in Google Drive. Set rawQuery=true to pass a raw Google Drive API query supporting operators like modifiedTime, createdTime, mimeType, name contains, etc.",
|
|
1069
1063
|
inputSchema: {
|
|
1070
1064
|
type: "object",
|
|
1071
1065
|
properties: {
|
|
1072
|
-
query: { type: "string", description: "Search query" },
|
|
1066
|
+
query: { type: "string", description: "Search query. When rawQuery=true, this is passed directly to the Google Drive API as the q parameter." },
|
|
1073
1067
|
pageSize: { type: "number", description: "Results per page (default 50, max 100)" },
|
|
1074
|
-
pageToken: { type: "string", description: "Token for next page of results" }
|
|
1068
|
+
pageToken: { type: "string", description: "Token for next page of results" },
|
|
1069
|
+
rawQuery: { type: "boolean", description: "If true, pass query directly to Google Drive API without wrapping in fullText contains. Enables date filters, mimeType filters, etc." }
|
|
1075
1070
|
},
|
|
1076
1071
|
required: ["query"]
|
|
1077
1072
|
}
|
|
@@ -1371,53 +1366,71 @@ var toolDefinitions = [
|
|
|
1371
1366
|
fileId: { type: "string", description: "Optional file ID for targeted access check" }
|
|
1372
1367
|
}
|
|
1373
1368
|
}
|
|
1374
|
-
},
|
|
1375
|
-
{
|
|
1376
|
-
name: "authClearTokens",
|
|
1377
|
-
description: "Clear saved OAuth tokens (requires confirm=true)",
|
|
1378
|
-
inputSchema: {
|
|
1379
|
-
type: "object",
|
|
1380
|
-
properties: {
|
|
1381
|
-
confirm: { type: "boolean", description: "Must be true to clear tokens" }
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
},
|
|
1385
|
-
{
|
|
1386
|
-
name: "authSuggestScopePreset",
|
|
1387
|
-
description: "Get scope configuration instructions for a preset",
|
|
1388
|
-
inputSchema: {
|
|
1389
|
-
type: "object",
|
|
1390
|
-
properties: {
|
|
1391
|
-
preset: { type: "string", enum: ["readonly", "content-editor", "full"], description: "Scope preset" },
|
|
1392
|
-
clearTokens: { type: "boolean", description: "Also clear saved tokens now" }
|
|
1393
|
-
},
|
|
1394
|
-
required: ["preset"]
|
|
1395
|
-
}
|
|
1396
1369
|
}
|
|
1397
1370
|
];
|
|
1398
1371
|
async function handleTool(toolName, args, ctx) {
|
|
1399
1372
|
switch (toolName) {
|
|
1400
1373
|
case "search": {
|
|
1374
|
+
let resolveParentPath2 = function(folderId, depth = 0) {
|
|
1375
|
+
if (depth >= 10) return Promise.resolve(folderId);
|
|
1376
|
+
if (folderId in pathCache) return pathCache[folderId];
|
|
1377
|
+
const promise = (async () => {
|
|
1378
|
+
try {
|
|
1379
|
+
const folderRes = await ctx.getDrive().files.get({
|
|
1380
|
+
fileId: folderId,
|
|
1381
|
+
fields: "name, parents",
|
|
1382
|
+
supportsAllDrives: true
|
|
1383
|
+
});
|
|
1384
|
+
const name = folderRes.data.name || folderId;
|
|
1385
|
+
const parents = folderRes.data.parents;
|
|
1386
|
+
if (parents && parents.length > 0 && parents[0] !== folderId) {
|
|
1387
|
+
const parentPath = await resolveParentPath2(parents[0], depth + 1);
|
|
1388
|
+
return `${parentPath}/${name}`;
|
|
1389
|
+
}
|
|
1390
|
+
return name;
|
|
1391
|
+
} catch {
|
|
1392
|
+
return folderId;
|
|
1393
|
+
}
|
|
1394
|
+
})();
|
|
1395
|
+
pathCache[folderId] = promise;
|
|
1396
|
+
return promise;
|
|
1397
|
+
};
|
|
1398
|
+
var resolveParentPath = resolveParentPath2;
|
|
1401
1399
|
const validation = SearchSchema.safeParse(args);
|
|
1402
1400
|
if (!validation.success) {
|
|
1403
1401
|
return errorResponse(validation.error.errors[0].message);
|
|
1404
1402
|
}
|
|
1405
|
-
const { query: userQuery, pageSize, pageToken } = validation.data;
|
|
1406
|
-
|
|
1407
|
-
|
|
1403
|
+
const { query: userQuery, pageSize, pageToken, rawQuery } = validation.data;
|
|
1404
|
+
let formattedQuery;
|
|
1405
|
+
if (rawQuery) {
|
|
1406
|
+
formattedQuery = /\btrashed\s*=/.test(userQuery) ? userQuery : `${userQuery} and trashed = false`;
|
|
1407
|
+
} else {
|
|
1408
|
+
const escapedQuery = escapeDriveQuery(userQuery);
|
|
1409
|
+
formattedQuery = `fullText contains '${escapedQuery}' and trashed = false`;
|
|
1410
|
+
}
|
|
1408
1411
|
const res = await ctx.getDrive().files.list({
|
|
1409
1412
|
q: formattedQuery,
|
|
1410
1413
|
pageSize: Math.min(pageSize || 50, 100),
|
|
1411
1414
|
pageToken,
|
|
1412
|
-
fields: "nextPageToken, files(id, name, mimeType, modifiedTime, size)",
|
|
1415
|
+
fields: "nextPageToken, files(id, name, mimeType, createdTime, modifiedTime, size, parents)",
|
|
1413
1416
|
corpora: "allDrives",
|
|
1414
1417
|
includeItemsFromAllDrives: true,
|
|
1415
1418
|
supportsAllDrives: true
|
|
1416
1419
|
});
|
|
1417
|
-
const
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1420
|
+
const pathCache = {};
|
|
1421
|
+
const files = res.data.files || [];
|
|
1422
|
+
const fileLines = await Promise.all(
|
|
1423
|
+
files.map(async (f) => {
|
|
1424
|
+
let folderPath = "";
|
|
1425
|
+
if (f.parents && f.parents.length > 0) {
|
|
1426
|
+
folderPath = await resolveParentPath2(f.parents[0]);
|
|
1427
|
+
}
|
|
1428
|
+
return `${f.name} (${f.mimeType}) [id: ${f.id}, path: ${folderPath || "/"}] [created: ${f.createdTime || "N/A"}, modified: ${f.modifiedTime || "N/A"}]`;
|
|
1429
|
+
})
|
|
1430
|
+
);
|
|
1431
|
+
ctx.log("Search results", { query: userQuery, rawQuery: !!rawQuery, resultCount: files.length });
|
|
1432
|
+
let response = `Found ${files.length} files:
|
|
1433
|
+
${fileLines.join("\n")}`;
|
|
1421
1434
|
if (res.data.nextPageToken) {
|
|
1422
1435
|
response += `
|
|
1423
1436
|
|
|
@@ -2206,62 +2219,6 @@ ${JSON.stringify({ message }, null, 2)}` }],
|
|
|
2206
2219
|
};
|
|
2207
2220
|
}
|
|
2208
2221
|
}
|
|
2209
|
-
case "authClearTokens": {
|
|
2210
|
-
const validation = AuthClearTokensSchema.safeParse(args);
|
|
2211
|
-
if (!validation.success) return errorResponse(validation.error.errors[0].message);
|
|
2212
|
-
const data = validation.data;
|
|
2213
|
-
if (!data.confirm) return errorResponse("Refusing token clear: set confirm=true.");
|
|
2214
|
-
const tokenPath = getSecureTokenPath();
|
|
2215
|
-
const existed = existsSync2(tokenPath);
|
|
2216
|
-
if (existed) {
|
|
2217
|
-
try {
|
|
2218
|
-
await unlink2(tokenPath);
|
|
2219
|
-
} catch (e) {
|
|
2220
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
2221
|
-
return errorResponse(`Failed to remove token file at ${tokenPath}: ${msg}`);
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
return {
|
|
2225
|
-
content: [{ type: "text", text: `Tokens cleared. File previously ${existed ? "existed" : "did not exist"} at ${tokenPath}. Re-run auth flow before next privileged operation.` }],
|
|
2226
|
-
isError: false
|
|
2227
|
-
};
|
|
2228
|
-
}
|
|
2229
|
-
case "authSuggestScopePreset": {
|
|
2230
|
-
const validation = AuthSetScopePresetSchema.safeParse(args);
|
|
2231
|
-
if (!validation.success) return errorResponse(validation.error.errors[0].message);
|
|
2232
|
-
const data = validation.data;
|
|
2233
|
-
const aliases = SCOPE_PRESETS[data.preset];
|
|
2234
|
-
const resolved = aliases.map((a) => SCOPE_ALIASES[a]);
|
|
2235
|
-
const envValue = aliases.join(",");
|
|
2236
|
-
let clearMessage = "";
|
|
2237
|
-
if (data.clearTokens) {
|
|
2238
|
-
const tokenPath = getSecureTokenPath();
|
|
2239
|
-
try {
|
|
2240
|
-
await unlink2(tokenPath);
|
|
2241
|
-
clearMessage = `
|
|
2242
|
-
Tokens cleared at ${tokenPath}.`;
|
|
2243
|
-
} catch (_e) {
|
|
2244
|
-
clearMessage = `
|
|
2245
|
-
Tokens clear requested, but no token file removed.`;
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
return {
|
|
2249
|
-
content: [{
|
|
2250
|
-
type: "text",
|
|
2251
|
-
text: `Scope preset selected: ${data.preset}
|
|
2252
|
-
Requested scopes: ${JSON.stringify(resolved, null, 2)}
|
|
2253
|
-
|
|
2254
|
-
Next steps:
|
|
2255
|
-
1) Export scope env:
|
|
2256
|
-
GOOGLE_DRIVE_MCP_SCOPES=${envValue}
|
|
2257
|
-
2) Restart MCP server
|
|
2258
|
-
3) Run auth flow to refresh consent (if prompted)
|
|
2259
|
-
|
|
2260
|
-
This tool does not mutate process env automatically.${clearMessage}`
|
|
2261
|
-
}],
|
|
2262
|
-
isError: false
|
|
2263
|
-
};
|
|
2264
|
-
}
|
|
2265
2222
|
default:
|
|
2266
2223
|
return null;
|
|
2267
2224
|
}
|