@piotr-agier/google-drive-mcp 1.6.0 → 1.7.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 +19 -0
- package/dist/index.js +357 -11
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -390,6 +390,25 @@ Add the server to your Claude Desktop configuration:
|
|
|
390
390
|
- `role`: Role (`reader`, `commenter`, `writer`)
|
|
391
391
|
- `sendNotificationEmail`: Send notification email (optional)
|
|
392
392
|
|
|
393
|
+
#### File Revisions (v1.7.0)
|
|
394
|
+
- **getRevisions** - List revisions for a file
|
|
395
|
+
- `fileId`: File ID
|
|
396
|
+
- `pageSize`: Max revisions to return (optional)
|
|
397
|
+
|
|
398
|
+
- **restoreRevision** - Restore a file from a selected revision (safety-confirmed)
|
|
399
|
+
- `fileId`: File ID
|
|
400
|
+
- `revisionId`: Revision ID to restore
|
|
401
|
+
- `confirm`: Must be `true` to execute restore
|
|
402
|
+
|
|
403
|
+
#### Auth Diagnostics & Scope Presets (v1.7.0)
|
|
404
|
+
- **authGetStatus** - Show token/scopes/auth health diagnostics (machine + human readable)
|
|
405
|
+
- **authListScopes** - Show configured/requested scopes, granted scopes, missing scopes, and presets
|
|
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
|
+
|
|
393
412
|
- **uploadFile** - Upload a local file (any type: image, audio, video, PDF, etc.) to Google Drive
|
|
394
413
|
- `localPath`: Absolute path to the local file
|
|
395
414
|
- `name`: File name in Drive (optional, defaults to local filename)
|
package/dist/index.js
CHANGED
|
@@ -351,16 +351,8 @@ var TokenManager = class {
|
|
|
351
351
|
|
|
352
352
|
// src/auth/server.ts
|
|
353
353
|
import open from "open";
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
"https://www.googleapis.com/auth/drive.file",
|
|
357
|
-
"https://www.googleapis.com/auth/drive.readonly",
|
|
358
|
-
"https://www.googleapis.com/auth/documents",
|
|
359
|
-
"https://www.googleapis.com/auth/spreadsheets",
|
|
360
|
-
"https://www.googleapis.com/auth/presentations",
|
|
361
|
-
"https://www.googleapis.com/auth/calendar",
|
|
362
|
-
"https://www.googleapis.com/auth/calendar.events"
|
|
363
|
-
];
|
|
354
|
+
|
|
355
|
+
// src/auth/scopes.ts
|
|
364
356
|
var SCOPE_ALIASES = {
|
|
365
357
|
drive: "https://www.googleapis.com/auth/drive",
|
|
366
358
|
"drive.file": "https://www.googleapis.com/auth/drive.file",
|
|
@@ -371,6 +363,21 @@ var SCOPE_ALIASES = {
|
|
|
371
363
|
calendar: "https://www.googleapis.com/auth/calendar",
|
|
372
364
|
"calendar.events": "https://www.googleapis.com/auth/calendar.events"
|
|
373
365
|
};
|
|
366
|
+
var SCOPE_PRESETS = {
|
|
367
|
+
readonly: ["drive.readonly"],
|
|
368
|
+
"content-editor": ["drive.file", "documents", "spreadsheets", "presentations"],
|
|
369
|
+
full: ["drive", "documents", "spreadsheets", "presentations", "calendar", "calendar.events"]
|
|
370
|
+
};
|
|
371
|
+
var DEFAULT_SCOPES = [
|
|
372
|
+
"drive",
|
|
373
|
+
"drive.file",
|
|
374
|
+
"drive.readonly",
|
|
375
|
+
"documents",
|
|
376
|
+
"spreadsheets",
|
|
377
|
+
"presentations",
|
|
378
|
+
"calendar",
|
|
379
|
+
"calendar.events"
|
|
380
|
+
].map((s) => SCOPE_ALIASES[s]);
|
|
374
381
|
function resolveOAuthScopes() {
|
|
375
382
|
const raw = process.env.GOOGLE_DRIVE_MCP_SCOPES?.trim();
|
|
376
383
|
if (!raw) return [...DEFAULT_SCOPES];
|
|
@@ -385,6 +392,8 @@ function resolveOAuthScopes() {
|
|
|
385
392
|
if (scopes.length === 0) return [...DEFAULT_SCOPES];
|
|
386
393
|
return [...new Set(scopes)];
|
|
387
394
|
}
|
|
395
|
+
|
|
396
|
+
// src/auth/server.ts
|
|
388
397
|
var SCOPES = resolveOAuthScopes();
|
|
389
398
|
var AuthServer = class {
|
|
390
399
|
// Flag for standalone script
|
|
@@ -690,7 +699,7 @@ __export(drive_exports, {
|
|
|
690
699
|
});
|
|
691
700
|
import { z } from "zod";
|
|
692
701
|
import { existsSync as existsSync2, statSync as statSync2, createReadStream } from "fs";
|
|
693
|
-
import { mkdtemp, readFile as readFile3, writeFile as writeFile2, rm } from "fs/promises";
|
|
702
|
+
import { mkdtemp, readFile as readFile3, writeFile as writeFile2, rm, unlink as unlink2 } from "fs/promises";
|
|
694
703
|
import { tmpdir } from "os";
|
|
695
704
|
import { basename as basename2, extname as extname2, join as join3 } from "path";
|
|
696
705
|
import { PDFDocument } from "pdf-lib";
|
|
@@ -1028,6 +1037,31 @@ async function splitPdfIntoChunkFiles(localPath, maxPagesPerChunk) {
|
|
|
1028
1037
|
}
|
|
1029
1038
|
return { tempDir, files };
|
|
1030
1039
|
}
|
|
1040
|
+
var GetRevisionsSchema = z.object({
|
|
1041
|
+
fileId: z.string().min(1, "File ID is required"),
|
|
1042
|
+
pageSize: z.number().int().min(1).max(200).optional().default(50),
|
|
1043
|
+
pageToken: z.string().optional()
|
|
1044
|
+
});
|
|
1045
|
+
var RestoreRevisionSchema = z.object({
|
|
1046
|
+
fileId: z.string().min(1, "File ID is required"),
|
|
1047
|
+
revisionId: z.string().min(1, "Revision ID is required"),
|
|
1048
|
+
confirm: z.boolean().optional().default(false)
|
|
1049
|
+
});
|
|
1050
|
+
var AuthTestFileAccessSchema = z.object({
|
|
1051
|
+
fileId: z.string().optional()
|
|
1052
|
+
});
|
|
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
|
+
function getGrantedScopesFromAuthClient(ctx) {
|
|
1061
|
+
const scopeRaw = ctx.authClient?.credentials?.scope;
|
|
1062
|
+
if (!scopeRaw || typeof scopeRaw !== "string") return [];
|
|
1063
|
+
return [...new Set(scopeRaw.split(" ").map((s) => s.trim()).filter(Boolean))];
|
|
1064
|
+
}
|
|
1031
1065
|
var toolDefinitions = [
|
|
1032
1066
|
{
|
|
1033
1067
|
name: "search",
|
|
@@ -1291,6 +1325,74 @@ var toolDefinitions = [
|
|
|
1291
1325
|
},
|
|
1292
1326
|
required: ["localPath"]
|
|
1293
1327
|
}
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
name: "getRevisions",
|
|
1331
|
+
description: "List revisions for a file",
|
|
1332
|
+
inputSchema: {
|
|
1333
|
+
type: "object",
|
|
1334
|
+
properties: {
|
|
1335
|
+
fileId: { type: "string", description: "Google Drive file ID" },
|
|
1336
|
+
pageSize: { type: "number", description: "Max revisions to return (default 50, max 200)" },
|
|
1337
|
+
pageToken: { type: "string", description: "Page token for pagination" }
|
|
1338
|
+
},
|
|
1339
|
+
required: ["fileId"]
|
|
1340
|
+
}
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
name: "restoreRevision",
|
|
1344
|
+
description: "Restore a file to a selected revision (creates a new head revision). Note: workspace files (Docs, Sheets, Slides) are restored via export/import and may lose some formatting.",
|
|
1345
|
+
inputSchema: {
|
|
1346
|
+
type: "object",
|
|
1347
|
+
properties: {
|
|
1348
|
+
fileId: { type: "string", description: "Google Drive file ID" },
|
|
1349
|
+
revisionId: { type: "string", description: "Revision ID to restore" },
|
|
1350
|
+
confirm: { type: "boolean", description: "Safety flag. Must be true to execute restore." }
|
|
1351
|
+
},
|
|
1352
|
+
required: ["fileId", "revisionId"]
|
|
1353
|
+
}
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
name: "authGetStatus",
|
|
1357
|
+
description: "Show authentication/token status and scope diagnostics",
|
|
1358
|
+
inputSchema: { type: "object", properties: {} }
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
name: "authListScopes",
|
|
1362
|
+
description: "List configured/requested scopes and currently granted scopes",
|
|
1363
|
+
inputSchema: { type: "object", properties: {} }
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
name: "authTestFileAccess",
|
|
1367
|
+
description: "Run auth diagnostics against Drive API/file access",
|
|
1368
|
+
inputSchema: {
|
|
1369
|
+
type: "object",
|
|
1370
|
+
properties: {
|
|
1371
|
+
fileId: { type: "string", description: "Optional file ID for targeted access check" }
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
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
|
+
}
|
|
1294
1396
|
}
|
|
1295
1397
|
];
|
|
1296
1398
|
async function handleTool(toolName, args, ctx) {
|
|
@@ -1308,6 +1410,7 @@ async function handleTool(toolName, args, ctx) {
|
|
|
1308
1410
|
pageSize: Math.min(pageSize || 50, 100),
|
|
1309
1411
|
pageToken,
|
|
1310
1412
|
fields: "nextPageToken, files(id, name, mimeType, modifiedTime, size)",
|
|
1413
|
+
corpora: "allDrives",
|
|
1311
1414
|
includeItemsFromAllDrives: true,
|
|
1312
1415
|
supportsAllDrives: true
|
|
1313
1416
|
});
|
|
@@ -1916,6 +2019,249 @@ ${lines.join("\n")}`
|
|
|
1916
2019
|
}
|
|
1917
2020
|
}
|
|
1918
2021
|
}
|
|
2022
|
+
case "getRevisions": {
|
|
2023
|
+
const validation = GetRevisionsSchema.safeParse(args);
|
|
2024
|
+
if (!validation.success) return errorResponse(validation.error.errors[0].message);
|
|
2025
|
+
const data = validation.data;
|
|
2026
|
+
const response = await ctx.getDrive().revisions.list({
|
|
2027
|
+
fileId: data.fileId,
|
|
2028
|
+
pageSize: data.pageSize,
|
|
2029
|
+
pageToken: data.pageToken,
|
|
2030
|
+
fields: "nextPageToken,revisions(id,modifiedTime,lastModifyingUser(displayName,emailAddress),keepForever,size,originalFilename)"
|
|
2031
|
+
});
|
|
2032
|
+
const revisions = response.data.revisions || [];
|
|
2033
|
+
if (revisions.length === 0) {
|
|
2034
|
+
return { content: [{ type: "text", text: `No revisions found for file ${data.fileId}.` }], isError: false };
|
|
2035
|
+
}
|
|
2036
|
+
const lines = revisions.map((r) => {
|
|
2037
|
+
const who = r.lastModifyingUser?.displayName || r.lastModifyingUser?.emailAddress || "unknown";
|
|
2038
|
+
return `- ${r.id}: ${r.modifiedTime || "unknown-time"} by ${who}${r.keepForever ? " [kept]" : ""}`;
|
|
2039
|
+
});
|
|
2040
|
+
let text = `Revisions for file ${data.fileId}:
|
|
2041
|
+
${lines.join("\n")}`;
|
|
2042
|
+
if (response.data.nextPageToken) {
|
|
2043
|
+
text += `
|
|
2044
|
+
|
|
2045
|
+
More revisions available. Use pageToken="${response.data.nextPageToken}" to fetch the next page.`;
|
|
2046
|
+
}
|
|
2047
|
+
return {
|
|
2048
|
+
content: [{ type: "text", text }],
|
|
2049
|
+
isError: false
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
case "restoreRevision": {
|
|
2053
|
+
const validation = RestoreRevisionSchema.safeParse(args);
|
|
2054
|
+
if (!validation.success) return errorResponse(validation.error.errors[0].message);
|
|
2055
|
+
const data = validation.data;
|
|
2056
|
+
if (!data.confirm) {
|
|
2057
|
+
return errorResponse("Refusing restore: set confirm=true to restore a revision.");
|
|
2058
|
+
}
|
|
2059
|
+
try {
|
|
2060
|
+
const current = await ctx.getDrive().files.get({
|
|
2061
|
+
fileId: data.fileId,
|
|
2062
|
+
fields: "name,mimeType",
|
|
2063
|
+
supportsAllDrives: true
|
|
2064
|
+
});
|
|
2065
|
+
const fileMimeType = current.data.mimeType || "";
|
|
2066
|
+
const isWorkspaceFile = fileMimeType.startsWith("application/vnd.google-apps.");
|
|
2067
|
+
let revisionBody;
|
|
2068
|
+
let uploadMimeType;
|
|
2069
|
+
if (isWorkspaceFile) {
|
|
2070
|
+
const revision = await ctx.getDrive().revisions.get({
|
|
2071
|
+
fileId: data.fileId,
|
|
2072
|
+
revisionId: data.revisionId,
|
|
2073
|
+
fields: "id,exportLinks"
|
|
2074
|
+
});
|
|
2075
|
+
const exportLinks = revision.data.exportLinks || {};
|
|
2076
|
+
const formatMap = GOOGLE_WORKSPACE_EXPORT_FORMATS[fileMimeType];
|
|
2077
|
+
const editableMimes = formatMap ? Object.entries(formatMap).filter(([ext]) => ext !== "pdf").map(([, mime]) => mime) : [];
|
|
2078
|
+
const selectedMime = editableMimes.find((m) => exportLinks[m]) || Object.keys(exportLinks).find((m) => m !== "application/pdf") || Object.keys(exportLinks)[0];
|
|
2079
|
+
if (!selectedMime || !exportLinks[selectedMime]) {
|
|
2080
|
+
return errorResponse("Selected revision has no usable export links for restore.");
|
|
2081
|
+
}
|
|
2082
|
+
uploadMimeType = selectedMime;
|
|
2083
|
+
const exportResponse = await ctx.authClient.request({ url: exportLinks[selectedMime], responseType: "stream" });
|
|
2084
|
+
revisionBody = exportResponse.data;
|
|
2085
|
+
} else {
|
|
2086
|
+
const revision = await ctx.getDrive().revisions.get(
|
|
2087
|
+
{ fileId: data.fileId, revisionId: data.revisionId, alt: "media" },
|
|
2088
|
+
{ responseType: "stream" }
|
|
2089
|
+
);
|
|
2090
|
+
revisionBody = revision.data;
|
|
2091
|
+
uploadMimeType = fileMimeType || "application/octet-stream";
|
|
2092
|
+
}
|
|
2093
|
+
await ctx.getDrive().files.update({
|
|
2094
|
+
fileId: data.fileId,
|
|
2095
|
+
media: {
|
|
2096
|
+
mimeType: uploadMimeType,
|
|
2097
|
+
body: revisionBody
|
|
2098
|
+
},
|
|
2099
|
+
supportsAllDrives: true
|
|
2100
|
+
});
|
|
2101
|
+
const restoreMsg = `Restored file ${data.fileId} (${current.data.name || "unnamed"}) from revision ${data.revisionId}.`;
|
|
2102
|
+
const workspaceWarning = isWorkspaceFile ? "\n\nWarning: This workspace file was restored via export/import. Some formatting or features (e.g. comments, suggestions, version history metadata) may have been lost." : "";
|
|
2103
|
+
return {
|
|
2104
|
+
content: [{
|
|
2105
|
+
type: "text",
|
|
2106
|
+
text: restoreMsg + workspaceWarning
|
|
2107
|
+
}],
|
|
2108
|
+
isError: false
|
|
2109
|
+
};
|
|
2110
|
+
} catch (err) {
|
|
2111
|
+
return errorResponse(`Failed to restore revision: ${err instanceof Error ? err.message : String(err)}`);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
case "authGetStatus": {
|
|
2115
|
+
const tokenPath = getSecureTokenPath();
|
|
2116
|
+
const tokenFileExists = existsSync2(tokenPath);
|
|
2117
|
+
let requestedScopes;
|
|
2118
|
+
try {
|
|
2119
|
+
requestedScopes = resolveOAuthScopes();
|
|
2120
|
+
} catch (e) {
|
|
2121
|
+
return errorResponse(`Invalid scope configuration: ${e instanceof Error ? e.message : String(e)}`);
|
|
2122
|
+
}
|
|
2123
|
+
const grantedScopes = getGrantedScopesFromAuthClient(ctx);
|
|
2124
|
+
const missingScopes = requestedScopes.filter((s) => !grantedScopes.includes(s));
|
|
2125
|
+
const expiryDate = ctx.authClient?.credentials?.expiry_date;
|
|
2126
|
+
const expiresInSec = expiryDate ? Math.floor((expiryDate - Date.now()) / 1e3) : null;
|
|
2127
|
+
const payload = {
|
|
2128
|
+
tokenFilePath: tokenPath,
|
|
2129
|
+
tokenFileExists,
|
|
2130
|
+
hasAccessToken: !!ctx.authClient?.credentials?.access_token,
|
|
2131
|
+
hasRefreshToken: !!ctx.authClient?.credentials?.refresh_token,
|
|
2132
|
+
expiryDate: expiryDate || null,
|
|
2133
|
+
expiresInSec,
|
|
2134
|
+
requestedScopes,
|
|
2135
|
+
grantedScopes,
|
|
2136
|
+
missingScopes
|
|
2137
|
+
};
|
|
2138
|
+
const status = !tokenFileExists || !payload.hasRefreshToken ? "needs_reauth" : missingScopes.length > 0 ? "scope_mismatch" : "ok";
|
|
2139
|
+
let text = `Auth status (${status}):
|
|
2140
|
+
${JSON.stringify(payload, null, 2)}
|
|
2141
|
+
|
|
2142
|
+
Summary: token file ${tokenFileExists ? "found" : "missing"}, missing scopes=${missingScopes.length}.`;
|
|
2143
|
+
if (grantedScopes.length === 0 && payload.hasAccessToken) {
|
|
2144
|
+
text += "\nNote: granted scopes may appear empty when the token was loaded from disk. This does not necessarily indicate missing permissions.";
|
|
2145
|
+
}
|
|
2146
|
+
return {
|
|
2147
|
+
content: [{ type: "text", text }],
|
|
2148
|
+
isError: false
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
case "authListScopes": {
|
|
2152
|
+
let requestedScopes;
|
|
2153
|
+
try {
|
|
2154
|
+
requestedScopes = resolveOAuthScopes();
|
|
2155
|
+
} catch (e) {
|
|
2156
|
+
return errorResponse(`Invalid scope configuration: ${e instanceof Error ? e.message : String(e)}`);
|
|
2157
|
+
}
|
|
2158
|
+
const grantedScopes = getGrantedScopesFromAuthClient(ctx);
|
|
2159
|
+
const missingScopes = requestedScopes.filter((s) => !grantedScopes.includes(s));
|
|
2160
|
+
const presetsResolved = Object.fromEntries(
|
|
2161
|
+
Object.entries(SCOPE_PRESETS).map(([k, v]) => [k, v.map((s) => SCOPE_ALIASES[s])])
|
|
2162
|
+
);
|
|
2163
|
+
let text = `Scopes:
|
|
2164
|
+
${JSON.stringify({ requestedScopes, grantedScopes, missingScopes, presets: presetsResolved }, null, 2)}`;
|
|
2165
|
+
if (grantedScopes.length === 0 && !!ctx.authClient?.credentials?.access_token) {
|
|
2166
|
+
text += "\nNote: granted scopes may appear empty when the token was loaded from disk. This does not necessarily indicate missing permissions.";
|
|
2167
|
+
}
|
|
2168
|
+
return {
|
|
2169
|
+
content: [{ type: "text", text }],
|
|
2170
|
+
isError: false
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
case "authTestFileAccess": {
|
|
2174
|
+
const validation = AuthTestFileAccessSchema.safeParse(args);
|
|
2175
|
+
if (!validation.success) return errorResponse(validation.error.errors[0].message);
|
|
2176
|
+
const data = validation.data;
|
|
2177
|
+
try {
|
|
2178
|
+
let check;
|
|
2179
|
+
if (data.fileId) {
|
|
2180
|
+
const file = await ctx.getDrive().files.get({
|
|
2181
|
+
fileId: data.fileId,
|
|
2182
|
+
fields: "id,name,mimeType,permissions",
|
|
2183
|
+
supportsAllDrives: true
|
|
2184
|
+
});
|
|
2185
|
+
check = { mode: "file", fileId: file.data.id, name: file.data.name, mimeType: file.data.mimeType };
|
|
2186
|
+
} else {
|
|
2187
|
+
const list = await ctx.getDrive().files.list({
|
|
2188
|
+
pageSize: 1,
|
|
2189
|
+
fields: "files(id,name,mimeType)",
|
|
2190
|
+
includeItemsFromAllDrives: true,
|
|
2191
|
+
supportsAllDrives: true
|
|
2192
|
+
});
|
|
2193
|
+
check = { mode: "list", visibleCount: list.data.files?.length || 0, sample: list.data.files?.[0] || null };
|
|
2194
|
+
}
|
|
2195
|
+
return {
|
|
2196
|
+
content: [{ type: "text", text: `Auth access check OK:
|
|
2197
|
+
${JSON.stringify(check, null, 2)}` }],
|
|
2198
|
+
isError: false
|
|
2199
|
+
};
|
|
2200
|
+
} catch (e) {
|
|
2201
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2202
|
+
return {
|
|
2203
|
+
content: [{ type: "text", text: `Auth access check failed:
|
|
2204
|
+
${JSON.stringify({ message }, null, 2)}` }],
|
|
2205
|
+
isError: true
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
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
|
+
}
|
|
1919
2265
|
default:
|
|
1920
2266
|
return null;
|
|
1921
2267
|
}
|