@piotr-agier/google-drive-mcp 1.7.0 → 1.7.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/README.md +22 -10
- package/dist/index.js +221 -123
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -145,7 +145,7 @@ npx @piotr-agier/google-drive-mcp auth
|
|
|
145
145
|
```bash
|
|
146
146
|
# Copy the example file
|
|
147
147
|
cp gcp-oauth.keys.example.json gcp-oauth.keys.json
|
|
148
|
-
|
|
148
|
+
|
|
149
149
|
# Edit gcp-oauth.keys.json with your OAuth client ID
|
|
150
150
|
```
|
|
151
151
|
|
|
@@ -153,7 +153,7 @@ npx @piotr-agier/google-drive-mcp auth
|
|
|
153
153
|
```bash
|
|
154
154
|
npm run auth
|
|
155
155
|
```
|
|
156
|
-
|
|
156
|
+
|
|
157
157
|
Note: Authentication happens automatically on first run of an MCP client if you skip this step.
|
|
158
158
|
|
|
159
159
|
## Docker Usage
|
|
@@ -164,7 +164,7 @@ npx @piotr-agier/google-drive-mcp auth
|
|
|
164
164
|
```bash
|
|
165
165
|
# Using npx
|
|
166
166
|
npx @piotr-agier/google-drive-mcp auth
|
|
167
|
-
|
|
167
|
+
|
|
168
168
|
# Or using local installation
|
|
169
169
|
npm run auth
|
|
170
170
|
```
|
|
@@ -322,9 +322,10 @@ Add the server to your Claude Desktop configuration:
|
|
|
322
322
|
|
|
323
323
|
### Search and Navigation
|
|
324
324
|
- **search** - Search for files across Google Drive
|
|
325
|
-
- `query`: Search terms
|
|
325
|
+
- `query`: Search terms (or raw Drive API query when `rawQuery=true`)
|
|
326
326
|
- `pageSize`: Number of results per page (optional, default 50, max 100)
|
|
327
327
|
- `pageToken`: Pagination token for next page (optional)
|
|
328
|
+
- `rawQuery`: Pass `query` directly to the Drive API — enables operators like `modifiedTime`, `createdTime`, `mimeType`, `name contains`, etc. (optional)
|
|
328
329
|
|
|
329
330
|
- **listFolder** - List contents of a folder
|
|
330
331
|
- `folderId`: Folder ID (optional, defaults to root)
|
|
@@ -400,20 +401,21 @@ Add the server to your Claude Desktop configuration:
|
|
|
400
401
|
- `revisionId`: Revision ID to restore
|
|
401
402
|
- `confirm`: Must be `true` to execute restore
|
|
402
403
|
|
|
403
|
-
#### Auth Diagnostics
|
|
404
|
+
#### Auth Diagnostics (v1.7.0)
|
|
404
405
|
- **authGetStatus** - Show token/scopes/auth health diagnostics (machine + human readable)
|
|
405
406
|
- **authListScopes** - Show configured/requested scopes, granted scopes, missing scopes, and presets
|
|
406
407
|
- **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
408
|
|
|
412
409
|
- **uploadFile** - Upload a local file (any type: image, audio, video, PDF, etc.) to Google Drive
|
|
413
410
|
- `localPath`: Absolute path to the local file
|
|
414
411
|
- `name`: File name in Drive (optional, defaults to local filename)
|
|
415
412
|
- `parentFolderId`: Parent folder ID or path (optional, e.g., '/Work/Projects')
|
|
416
413
|
- `mimeType`: MIME type (optional, auto-detected from extension)
|
|
414
|
+
- `convertToGoogleFormat`: Convert uploaded file to native Google Workspace format (optional, default: false). When enabled, Office files are automatically converted:
|
|
415
|
+
- `.docx` / `.doc` → Google Doc
|
|
416
|
+
- `.xlsx` / `.xls` → Google Sheet
|
|
417
|
+
- `.pptx` / `.ppt` → Google Slides
|
|
418
|
+
- File extension is stripped from the name automatically (e.g., `report.docx` becomes `report`)
|
|
417
419
|
|
|
418
420
|
- **downloadFile** - Download a Google Drive file to a local path
|
|
419
421
|
- `fileId`: Google Drive file ID
|
|
@@ -533,6 +535,15 @@ Add the server to your Claude Desktop configuration:
|
|
|
533
535
|
- **formatGoogleDocParagraph** - Alias for `applyParagraphStyle` (compatibility helper)
|
|
534
536
|
- Same parameters as `applyParagraphStyle`
|
|
535
537
|
|
|
538
|
+
#### Bullet Points and Lists
|
|
539
|
+
- **createParagraphBullets** - Add or remove bullet points / numbered lists on paragraphs
|
|
540
|
+
- `documentId`: Document ID
|
|
541
|
+
- Target (use one): `startIndex`+`endIndex` OR `textToFind`+`matchInstance`
|
|
542
|
+
- `bulletPreset`: Bullet style preset (optional, default: `BULLET_DISC_CIRCLE_SQUARE`). Available presets:
|
|
543
|
+
- **Bullet styles**: `BULLET_DISC_CIRCLE_SQUARE`, `BULLET_DIAMONDX_ARROW3D_SQUARE`, `BULLET_CHECKBOX`, `BULLET_ARROW_DIAMOND_DISC`, `BULLET_STAR_CIRCLE_SQUARE`, `BULLET_ARROW3D_CIRCLE_SQUARE`, `BULLET_LEFTTRIANGLE_DIAMOND_DISC`
|
|
544
|
+
- **Numbered styles**: `NUMBERED_DECIMAL_ALPHA_ROMAN`, `NUMBERED_DECIMAL_ALPHA_ROMAN_PARENS`, `NUMBERED_DECIMAL_NESTED`, `NUMBERED_UPPERALPHA_ALPHA_ROMAN`, `NUMBERED_UPPERROMAN_UPPERALPHA_DECIMAL`, `NUMBERED_ZERODECIMAL_ALPHA_ROMAN`
|
|
545
|
+
- **Remove bullets**: `NONE` — removes existing bullets/numbering from the targeted paragraphs
|
|
546
|
+
|
|
536
547
|
- **findAndReplaceInDoc** - Find and replace text across a Google Doc
|
|
537
548
|
- `documentId`: Document ID
|
|
538
549
|
- `findText`: Text to find
|
|
@@ -589,6 +600,7 @@ Add the server to your Claude Desktop configuration:
|
|
|
589
600
|
- `documentId`: Document ID
|
|
590
601
|
- `commentId`: Comment ID to reply to
|
|
591
602
|
- `replyText`: The reply content
|
|
603
|
+
- `resolve`: Set to `true` to resolve the comment thread after replying (optional, default: false)
|
|
592
604
|
|
|
593
605
|
- **deleteComment** - Delete a comment from the document
|
|
594
606
|
- `documentId`: Document ID
|
|
@@ -982,7 +994,7 @@ npx @piotr-agier/google-drive-mcp auth
|
|
|
982
994
|
|
|
983
995
|
**Solution:**
|
|
984
996
|
1. Go to [Google Account Permissions](https://myaccount.google.com/permissions)
|
|
985
|
-
2. Find and remove access for "Google Drive MCP"
|
|
997
|
+
2. Find and remove access for "Google Drive MCP"
|
|
986
998
|
3. Clear local tokens: `rm ~/.config/google-drive-mcp/tokens.json`
|
|
987
999
|
4. Re-authenticate to grant all required scopes
|
|
988
1000
|
5. Verify the consent screen shows ALL scopes including full Drive access
|
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"),
|
|
@@ -961,7 +962,8 @@ var UploadFileSchema = z.object({
|
|
|
961
962
|
localPath: z.string().min(1, "Local file path is required"),
|
|
962
963
|
name: z.string().optional(),
|
|
963
964
|
parentFolderId: z.string().optional(),
|
|
964
|
-
mimeType: z.string().optional()
|
|
965
|
+
mimeType: z.string().optional(),
|
|
966
|
+
convertToGoogleFormat: z.boolean().optional()
|
|
965
967
|
});
|
|
966
968
|
var DownloadFileSchema = z.object({
|
|
967
969
|
fileId: z.string().min(1, "File ID is required"),
|
|
@@ -1050,28 +1052,28 @@ var RestoreRevisionSchema = z.object({
|
|
|
1050
1052
|
var AuthTestFileAccessSchema = z.object({
|
|
1051
1053
|
fileId: z.string().optional()
|
|
1052
1054
|
});
|
|
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
1055
|
function getGrantedScopesFromAuthClient(ctx) {
|
|
1061
1056
|
const scopeRaw = ctx.authClient?.credentials?.scope;
|
|
1062
1057
|
if (!scopeRaw || typeof scopeRaw !== "string") return [];
|
|
1063
1058
|
return [...new Set(scopeRaw.split(" ").map((s) => s.trim()).filter(Boolean))];
|
|
1064
1059
|
}
|
|
1060
|
+
function resolveScopeStatus(ctx) {
|
|
1061
|
+
const requestedScopes = resolveOAuthScopes();
|
|
1062
|
+
const grantedScopes = getGrantedScopesFromAuthClient(ctx);
|
|
1063
|
+
const missingScopes = requestedScopes.filter((s) => !grantedScopes.includes(s));
|
|
1064
|
+
return { requestedScopes, grantedScopes, missingScopes };
|
|
1065
|
+
}
|
|
1065
1066
|
var toolDefinitions = [
|
|
1066
1067
|
{
|
|
1067
1068
|
name: "search",
|
|
1068
|
-
description: "Search for files in Google Drive",
|
|
1069
|
+
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
1070
|
inputSchema: {
|
|
1070
1071
|
type: "object",
|
|
1071
1072
|
properties: {
|
|
1072
|
-
query: { type: "string", description: "Search query" },
|
|
1073
|
+
query: { type: "string", description: "Search query. When rawQuery=true, this is passed directly to the Google Drive API as the q parameter." },
|
|
1073
1074
|
pageSize: { type: "number", description: "Results per page (default 50, max 100)" },
|
|
1074
|
-
pageToken: { type: "string", description: "Token for next page of results" }
|
|
1075
|
+
pageToken: { type: "string", description: "Token for next page of results" },
|
|
1076
|
+
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
1077
|
},
|
|
1076
1078
|
required: ["query"]
|
|
1077
1079
|
}
|
|
@@ -1194,7 +1196,8 @@ var toolDefinitions = [
|
|
|
1194
1196
|
localPath: { type: "string", description: "Absolute path to the local file to upload" },
|
|
1195
1197
|
name: { type: "string", description: "File name in Drive (defaults to local filename)" },
|
|
1196
1198
|
parentFolderId: { type: "string", description: "Parent folder ID or path (e.g., '/Work/Projects'). Creates folders if needed. Defaults to root." },
|
|
1197
|
-
mimeType: { type: "string", description: "MIME type (auto-detected from extension if omitted)" }
|
|
1199
|
+
mimeType: { type: "string", description: "MIME type (auto-detected from extension if omitted)" },
|
|
1200
|
+
convertToGoogleFormat: { type: "boolean", description: "Convert uploaded file to Google Workspace format (e.g., .docx to Google Doc, .xlsx to Google Sheet, .pptx to Google Slides). Defaults to false." }
|
|
1198
1201
|
},
|
|
1199
1202
|
required: ["localPath"]
|
|
1200
1203
|
}
|
|
@@ -1371,53 +1374,71 @@ var toolDefinitions = [
|
|
|
1371
1374
|
fileId: { type: "string", description: "Optional file ID for targeted access check" }
|
|
1372
1375
|
}
|
|
1373
1376
|
}
|
|
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
1377
|
}
|
|
1397
1378
|
];
|
|
1398
1379
|
async function handleTool(toolName, args, ctx) {
|
|
1399
1380
|
switch (toolName) {
|
|
1400
1381
|
case "search": {
|
|
1382
|
+
let resolveParentPath2 = function(folderId, depth = 0) {
|
|
1383
|
+
if (depth >= 10) return Promise.resolve(folderId);
|
|
1384
|
+
if (folderId in pathCache) return pathCache[folderId];
|
|
1385
|
+
const promise = (async () => {
|
|
1386
|
+
try {
|
|
1387
|
+
const folderRes = await ctx.getDrive().files.get({
|
|
1388
|
+
fileId: folderId,
|
|
1389
|
+
fields: "name, parents",
|
|
1390
|
+
supportsAllDrives: true
|
|
1391
|
+
});
|
|
1392
|
+
const name = folderRes.data.name || folderId;
|
|
1393
|
+
const parents = folderRes.data.parents;
|
|
1394
|
+
if (parents && parents.length > 0 && parents[0] !== folderId) {
|
|
1395
|
+
const parentPath = await resolveParentPath2(parents[0], depth + 1);
|
|
1396
|
+
return `${parentPath}/${name}`;
|
|
1397
|
+
}
|
|
1398
|
+
return name;
|
|
1399
|
+
} catch {
|
|
1400
|
+
return folderId;
|
|
1401
|
+
}
|
|
1402
|
+
})();
|
|
1403
|
+
pathCache[folderId] = promise;
|
|
1404
|
+
return promise;
|
|
1405
|
+
};
|
|
1406
|
+
var resolveParentPath = resolveParentPath2;
|
|
1401
1407
|
const validation = SearchSchema.safeParse(args);
|
|
1402
1408
|
if (!validation.success) {
|
|
1403
1409
|
return errorResponse(validation.error.errors[0].message);
|
|
1404
1410
|
}
|
|
1405
|
-
const { query: userQuery, pageSize, pageToken } = validation.data;
|
|
1406
|
-
|
|
1407
|
-
|
|
1411
|
+
const { query: userQuery, pageSize, pageToken, rawQuery } = validation.data;
|
|
1412
|
+
let formattedQuery;
|
|
1413
|
+
if (rawQuery) {
|
|
1414
|
+
formattedQuery = /\btrashed\s*=/.test(userQuery) ? userQuery : `${userQuery} and trashed = false`;
|
|
1415
|
+
} else {
|
|
1416
|
+
const escapedQuery = escapeDriveQuery(userQuery);
|
|
1417
|
+
formattedQuery = `fullText contains '${escapedQuery}' and trashed = false`;
|
|
1418
|
+
}
|
|
1408
1419
|
const res = await ctx.getDrive().files.list({
|
|
1409
1420
|
q: formattedQuery,
|
|
1410
1421
|
pageSize: Math.min(pageSize || 50, 100),
|
|
1411
1422
|
pageToken,
|
|
1412
|
-
fields: "nextPageToken, files(id, name, mimeType, modifiedTime, size)",
|
|
1423
|
+
fields: "nextPageToken, files(id, name, mimeType, createdTime, modifiedTime, size, parents)",
|
|
1413
1424
|
corpora: "allDrives",
|
|
1414
1425
|
includeItemsFromAllDrives: true,
|
|
1415
1426
|
supportsAllDrives: true
|
|
1416
1427
|
});
|
|
1417
|
-
const
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1428
|
+
const pathCache = {};
|
|
1429
|
+
const files = res.data.files || [];
|
|
1430
|
+
const fileLines = await Promise.all(
|
|
1431
|
+
files.map(async (f) => {
|
|
1432
|
+
let folderPath = "";
|
|
1433
|
+
if (f.parents && f.parents.length > 0) {
|
|
1434
|
+
folderPath = await resolveParentPath2(f.parents[0]);
|
|
1435
|
+
}
|
|
1436
|
+
return `${f.name} (${f.mimeType}) [id: ${f.id}, path: ${folderPath || "/"}] [created: ${f.createdTime || "N/A"}, modified: ${f.modifiedTime || "N/A"}]`;
|
|
1437
|
+
})
|
|
1438
|
+
);
|
|
1439
|
+
ctx.log("Search results", { query: userQuery, rawQuery: !!rawQuery, resultCount: files.length });
|
|
1440
|
+
let response = `Found ${files.length} files:
|
|
1441
|
+
${fileLines.join("\n")}`;
|
|
1421
1442
|
if (res.data.nextPageToken) {
|
|
1422
1443
|
response += `
|
|
1423
1444
|
|
|
@@ -1721,12 +1742,31 @@ Link: ${response.data.webViewLink}` }],
|
|
|
1721
1742
|
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
1722
1743
|
const detectedMime = data.mimeType || BINARY_MIME_TYPES[ext] || "application/octet-stream";
|
|
1723
1744
|
const parentId = await ctx.resolveFolderId(data.parentFolderId);
|
|
1724
|
-
|
|
1745
|
+
const GOOGLE_FORMAT_MAP = {
|
|
1746
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "application/vnd.google-apps.document",
|
|
1747
|
+
"application/msword": "application/vnd.google-apps.document",
|
|
1748
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "application/vnd.google-apps.spreadsheet",
|
|
1749
|
+
"application/vnd.ms-excel": "application/vnd.google-apps.spreadsheet",
|
|
1750
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "application/vnd.google-apps.presentation",
|
|
1751
|
+
"application/vnd.ms-powerpoint": "application/vnd.google-apps.presentation"
|
|
1752
|
+
};
|
|
1753
|
+
const targetMimeType = data.convertToGoogleFormat ? GOOGLE_FORMAT_MAP[detectedMime] : void 0;
|
|
1754
|
+
if (data.convertToGoogleFormat && !targetMimeType) {
|
|
1755
|
+
return errorResponse(
|
|
1756
|
+
`Cannot convert MIME type "${detectedMime}" to a Google Workspace format. Supported: .docx, .doc, .xlsx, .xls, .pptx, .ppt`
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
const uploadName = targetMimeType ? fileName.replace(/\.[^.]+$/, "") : fileName;
|
|
1760
|
+
ctx.log("Uploading file", { localPath: data.localPath, name: uploadName, mimeType: detectedMime, convertToGoogle: !!targetMimeType, size: stats.size });
|
|
1761
|
+
const requestBody = {
|
|
1762
|
+
name: uploadName,
|
|
1763
|
+
parents: [parentId]
|
|
1764
|
+
};
|
|
1765
|
+
if (targetMimeType) {
|
|
1766
|
+
requestBody.mimeType = targetMimeType;
|
|
1767
|
+
}
|
|
1725
1768
|
const file = await ctx.getDrive().files.create({
|
|
1726
|
-
requestBody
|
|
1727
|
-
name: fileName,
|
|
1728
|
-
parents: [parentId]
|
|
1729
|
-
},
|
|
1769
|
+
requestBody,
|
|
1730
1770
|
media: {
|
|
1731
1771
|
mimeType: detectedMime,
|
|
1732
1772
|
body: createReadStream(data.localPath)
|
|
@@ -1930,7 +1970,7 @@ Link: ${converted.data.webViewLink}` }], isError: false };
|
|
|
1930
1970
|
if (!validation.success) return errorResponse(validation.error.errors[0].message);
|
|
1931
1971
|
const data = validation.data;
|
|
1932
1972
|
const list = await ctx.getDrive().files.list({
|
|
1933
|
-
q: `'${
|
|
1973
|
+
q: `'${data.folderId}' in parents and mimeType='application/pdf' and trashed=false`,
|
|
1934
1974
|
pageSize: data.maxResults,
|
|
1935
1975
|
fields: "files(id,name,mimeType)",
|
|
1936
1976
|
includeItemsFromAllDrives: true,
|
|
@@ -1950,10 +1990,10 @@ Link: ${converted.data.webViewLink}` }], isError: false };
|
|
|
1950
1990
|
fields: "id,name",
|
|
1951
1991
|
supportsAllDrives: true
|
|
1952
1992
|
});
|
|
1953
|
-
results.push({ id: f.id
|
|
1993
|
+
results.push({ id: f.id ?? void 0, name: f.name ?? void 0, docId: converted.data.id ?? void 0, ok: true });
|
|
1954
1994
|
} catch (err) {
|
|
1955
1995
|
const message = err?.message || "Unknown conversion error";
|
|
1956
|
-
results.push({ id: f.id
|
|
1996
|
+
results.push({ id: f.id ?? void 0, name: f.name ?? void 0, ok: false, error: message });
|
|
1957
1997
|
if (!data.continueOnError) break;
|
|
1958
1998
|
}
|
|
1959
1999
|
}
|
|
@@ -1973,7 +2013,7 @@ ${results.map((r) => r.ok ? `\u2705 ${r.name} -> ${r.docId}` : `\u274C ${r.name}
|
|
|
1973
2013
|
if (!existsSync2(data.localPath)) return errorResponse(`File not found: ${data.localPath}`);
|
|
1974
2014
|
const parentId = await ctx.resolveFolderId(data.parentFolderId);
|
|
1975
2015
|
if (!data.split) {
|
|
1976
|
-
const fileName = data.namePrefix || data.localPath
|
|
2016
|
+
const fileName = data.namePrefix || basename2(data.localPath) || "upload.pdf";
|
|
1977
2017
|
const uploaded = await ctx.getDrive().files.create({
|
|
1978
2018
|
requestBody: { name: fileName, parents: [parentId] },
|
|
1979
2019
|
media: { mimeType: "application/pdf", body: createReadStream(data.localPath) },
|
|
@@ -2114,14 +2154,13 @@ More revisions available. Use pageToken="${response.data.nextPageToken}" to fetc
|
|
|
2114
2154
|
case "authGetStatus": {
|
|
2115
2155
|
const tokenPath = getSecureTokenPath();
|
|
2116
2156
|
const tokenFileExists = existsSync2(tokenPath);
|
|
2117
|
-
let
|
|
2157
|
+
let scopeStatus;
|
|
2118
2158
|
try {
|
|
2119
|
-
|
|
2159
|
+
scopeStatus = resolveScopeStatus(ctx);
|
|
2120
2160
|
} catch (e) {
|
|
2121
2161
|
return errorResponse(`Invalid scope configuration: ${e instanceof Error ? e.message : String(e)}`);
|
|
2122
2162
|
}
|
|
2123
|
-
const grantedScopes =
|
|
2124
|
-
const missingScopes = requestedScopes.filter((s) => !grantedScopes.includes(s));
|
|
2163
|
+
const { requestedScopes, grantedScopes, missingScopes } = scopeStatus;
|
|
2125
2164
|
const expiryDate = ctx.authClient?.credentials?.expiry_date;
|
|
2126
2165
|
const expiresInSec = expiryDate ? Math.floor((expiryDate - Date.now()) / 1e3) : null;
|
|
2127
2166
|
const payload = {
|
|
@@ -2149,16 +2188,15 @@ Summary: token file ${tokenFileExists ? "found" : "missing"}, missing scopes=${m
|
|
|
2149
2188
|
};
|
|
2150
2189
|
}
|
|
2151
2190
|
case "authListScopes": {
|
|
2152
|
-
let
|
|
2191
|
+
let scopeStatus;
|
|
2153
2192
|
try {
|
|
2154
|
-
|
|
2193
|
+
scopeStatus = resolveScopeStatus(ctx);
|
|
2155
2194
|
} catch (e) {
|
|
2156
2195
|
return errorResponse(`Invalid scope configuration: ${e instanceof Error ? e.message : String(e)}`);
|
|
2157
2196
|
}
|
|
2158
|
-
const grantedScopes =
|
|
2159
|
-
const missingScopes = requestedScopes.filter((s) => !grantedScopes.includes(s));
|
|
2197
|
+
const { requestedScopes, grantedScopes, missingScopes } = scopeStatus;
|
|
2160
2198
|
const presetsResolved = Object.fromEntries(
|
|
2161
|
-
Object.entries(SCOPE_PRESETS).map(([k, v]) => [k, v.map((s) => SCOPE_ALIASES[s])])
|
|
2199
|
+
Object.entries(SCOPE_PRESETS).map(([k, v]) => [k, v.map((s) => SCOPE_ALIASES[s] || s)])
|
|
2162
2200
|
);
|
|
2163
2201
|
let text = `Scopes:
|
|
2164
2202
|
${JSON.stringify({ requestedScopes, grantedScopes, missingScopes, presets: presetsResolved }, null, 2)}`;
|
|
@@ -2206,62 +2244,6 @@ ${JSON.stringify({ message }, null, 2)}` }],
|
|
|
2206
2244
|
};
|
|
2207
2245
|
}
|
|
2208
2246
|
}
|
|
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
2247
|
default:
|
|
2266
2248
|
return null;
|
|
2267
2249
|
}
|
|
@@ -2674,6 +2656,29 @@ var ApplyParagraphStyleSchema = z2.object({
|
|
|
2674
2656
|
namedStyleType: z2.enum(["NORMAL_TEXT", "TITLE", "SUBTITLE", "HEADING_1", "HEADING_2", "HEADING_3", "HEADING_4", "HEADING_5", "HEADING_6"]).optional(),
|
|
2675
2657
|
keepWithNext: z2.boolean().optional()
|
|
2676
2658
|
});
|
|
2659
|
+
var CreateParagraphBulletsSchema = z2.object({
|
|
2660
|
+
documentId: z2.string().min(1, "Document ID is required"),
|
|
2661
|
+
startIndex: z2.number().int().min(1).optional(),
|
|
2662
|
+
endIndex: z2.number().int().min(1).optional(),
|
|
2663
|
+
textToFind: z2.string().min(1).optional(),
|
|
2664
|
+
matchInstance: z2.number().int().min(1).optional().default(1),
|
|
2665
|
+
bulletPreset: z2.enum([
|
|
2666
|
+
"BULLET_DISC_CIRCLE_SQUARE",
|
|
2667
|
+
"BULLET_DIAMONDX_ARROW3D_SQUARE",
|
|
2668
|
+
"BULLET_CHECKBOX",
|
|
2669
|
+
"BULLET_ARROW_DIAMOND_DISC",
|
|
2670
|
+
"BULLET_STAR_CIRCLE_SQUARE",
|
|
2671
|
+
"BULLET_ARROW3D_CIRCLE_SQUARE",
|
|
2672
|
+
"BULLET_LEFTTRIANGLE_DIAMOND_DISC",
|
|
2673
|
+
"NUMBERED_DECIMAL_ALPHA_ROMAN",
|
|
2674
|
+
"NUMBERED_DECIMAL_ALPHA_ROMAN_PARENS",
|
|
2675
|
+
"NUMBERED_DECIMAL_NESTED",
|
|
2676
|
+
"NUMBERED_UPPERALPHA_ALPHA_ROMAN",
|
|
2677
|
+
"NUMBERED_UPPERROMAN_UPPERALPHA_DECIMAL",
|
|
2678
|
+
"NUMBERED_ZERODECIMAL_ALPHA_ROMAN",
|
|
2679
|
+
"NONE"
|
|
2680
|
+
]).default("BULLET_DISC_CIRCLE_SQUARE")
|
|
2681
|
+
});
|
|
2677
2682
|
var ListCommentsSchema = z2.object({
|
|
2678
2683
|
documentId: z2.string().min(1, "Document ID is required"),
|
|
2679
2684
|
includeDeleted: z2.boolean().optional(),
|
|
@@ -2693,7 +2698,8 @@ var AddCommentSchema = z2.object({
|
|
|
2693
2698
|
var ReplyToCommentSchema = z2.object({
|
|
2694
2699
|
documentId: z2.string().min(1, "Document ID is required"),
|
|
2695
2700
|
commentId: z2.string().min(1, "Comment ID is required"),
|
|
2696
|
-
replyText: z2.string().min(1, "Reply text is required")
|
|
2701
|
+
replyText: z2.string().min(1, "Reply text is required"),
|
|
2702
|
+
resolve: z2.boolean().optional().describe("Set to true to resolve the comment thread after replying")
|
|
2697
2703
|
});
|
|
2698
2704
|
var DeleteCommentSchema = z2.object({
|
|
2699
2705
|
documentId: z2.string().min(1, "Document ID is required"),
|
|
@@ -2937,6 +2943,22 @@ var toolDefinitions2 = [
|
|
|
2937
2943
|
required: ["documentId"]
|
|
2938
2944
|
}
|
|
2939
2945
|
},
|
|
2946
|
+
{
|
|
2947
|
+
name: "createParagraphBullets",
|
|
2948
|
+
description: "Add or remove bullet points / numbered lists on paragraphs in a Google Doc. Target paragraphs by startIndex+endIndex or textToFind. Use bulletPreset='NONE' to remove bullets.",
|
|
2949
|
+
inputSchema: {
|
|
2950
|
+
type: "object",
|
|
2951
|
+
properties: {
|
|
2952
|
+
documentId: { type: "string", description: "The document ID" },
|
|
2953
|
+
startIndex: { type: "number", description: "Start index (1-based) - use with endIndex" },
|
|
2954
|
+
endIndex: { type: "number", description: "End index (exclusive) - use with startIndex" },
|
|
2955
|
+
textToFind: { type: "string", description: "Text within the target paragraph(s) to bulletize" },
|
|
2956
|
+
matchInstance: { type: "number", description: "Which instance of textToFind (default: 1)" },
|
|
2957
|
+
bulletPreset: { type: "string", enum: ["BULLET_DISC_CIRCLE_SQUARE", "BULLET_DIAMONDX_ARROW3D_SQUARE", "BULLET_CHECKBOX", "BULLET_ARROW_DIAMOND_DISC", "BULLET_STAR_CIRCLE_SQUARE", "BULLET_ARROW3D_CIRCLE_SQUARE", "BULLET_LEFTTRIANGLE_DIAMOND_DISC", "NUMBERED_DECIMAL_ALPHA_ROMAN", "NUMBERED_DECIMAL_ALPHA_ROMAN_PARENS", "NUMBERED_DECIMAL_NESTED", "NUMBERED_UPPERALPHA_ALPHA_ROMAN", "NUMBERED_UPPERROMAN_UPPERALPHA_DECIMAL", "NUMBERED_ZERODECIMAL_ALPHA_ROMAN", "NONE"], description: "Bullet style preset. Use NONE to remove bullets. Default: BULLET_DISC_CIRCLE_SQUARE" }
|
|
2958
|
+
},
|
|
2959
|
+
required: ["documentId"]
|
|
2960
|
+
}
|
|
2961
|
+
},
|
|
2940
2962
|
{
|
|
2941
2963
|
name: "findAndReplaceInDoc",
|
|
2942
2964
|
description: "Find and replace text across a Google Document. Dry-run mode counts matches from paragraph text only (may differ from actual replacements which cover tables, headers, footers, etc.)",
|
|
@@ -3000,7 +3022,8 @@ var toolDefinitions2 = [
|
|
|
3000
3022
|
properties: {
|
|
3001
3023
|
documentId: { type: "string", description: "The document ID" },
|
|
3002
3024
|
commentId: { type: "string", description: "The comment ID to reply to" },
|
|
3003
|
-
replyText: { type: "string", description: "The reply content" }
|
|
3025
|
+
replyText: { type: "string", description: "The reply content" },
|
|
3026
|
+
resolve: { type: "boolean", description: "Set to true to resolve the comment thread after replying (default: false)" }
|
|
3004
3027
|
},
|
|
3005
3028
|
required: ["documentId", "commentId", "replyText"]
|
|
3006
3029
|
}
|
|
@@ -3791,6 +3814,65 @@ ${text}`;
|
|
|
3791
3814
|
isError: false
|
|
3792
3815
|
};
|
|
3793
3816
|
}
|
|
3817
|
+
case "createParagraphBullets": {
|
|
3818
|
+
const validation = CreateParagraphBulletsSchema.safeParse(args);
|
|
3819
|
+
if (!validation.success) {
|
|
3820
|
+
return errorResponse(validation.error.errors[0].message);
|
|
3821
|
+
}
|
|
3822
|
+
const a = validation.data;
|
|
3823
|
+
let startIndex;
|
|
3824
|
+
let endIndex;
|
|
3825
|
+
if (a.startIndex !== void 0 && a.endIndex !== void 0) {
|
|
3826
|
+
startIndex = a.startIndex;
|
|
3827
|
+
endIndex = a.endIndex;
|
|
3828
|
+
} else if (a.textToFind !== void 0) {
|
|
3829
|
+
const range = await findTextRange(
|
|
3830
|
+
ctx,
|
|
3831
|
+
a.documentId,
|
|
3832
|
+
a.textToFind,
|
|
3833
|
+
a.matchInstance || 1
|
|
3834
|
+
);
|
|
3835
|
+
if (!range) {
|
|
3836
|
+
return errorResponse(`Text "${a.textToFind}" not found in document`);
|
|
3837
|
+
}
|
|
3838
|
+
startIndex = range.startIndex;
|
|
3839
|
+
endIndex = range.endIndex;
|
|
3840
|
+
} else {
|
|
3841
|
+
return errorResponse("Must provide either startIndex+endIndex or textToFind");
|
|
3842
|
+
}
|
|
3843
|
+
const docs = ctx.google.docs({ version: "v1", auth: ctx.authClient });
|
|
3844
|
+
if (a.bulletPreset === "NONE") {
|
|
3845
|
+
await docs.documents.batchUpdate({
|
|
3846
|
+
documentId: a.documentId,
|
|
3847
|
+
requestBody: {
|
|
3848
|
+
requests: [{
|
|
3849
|
+
deleteParagraphBullets: {
|
|
3850
|
+
range: { startIndex, endIndex }
|
|
3851
|
+
}
|
|
3852
|
+
}]
|
|
3853
|
+
}
|
|
3854
|
+
});
|
|
3855
|
+
return {
|
|
3856
|
+
content: [{ type: "text", text: `Removed bullets from range ${startIndex}-${endIndex}` }],
|
|
3857
|
+
isError: false
|
|
3858
|
+
};
|
|
3859
|
+
}
|
|
3860
|
+
await docs.documents.batchUpdate({
|
|
3861
|
+
documentId: a.documentId,
|
|
3862
|
+
requestBody: {
|
|
3863
|
+
requests: [{
|
|
3864
|
+
createParagraphBullets: {
|
|
3865
|
+
range: { startIndex, endIndex },
|
|
3866
|
+
bulletPreset: a.bulletPreset
|
|
3867
|
+
}
|
|
3868
|
+
}]
|
|
3869
|
+
}
|
|
3870
|
+
});
|
|
3871
|
+
return {
|
|
3872
|
+
content: [{ type: "text", text: `Applied ${a.bulletPreset} bullets to range ${startIndex}-${endIndex}` }],
|
|
3873
|
+
isError: false
|
|
3874
|
+
};
|
|
3875
|
+
}
|
|
3794
3876
|
case "findAndReplaceInDoc": {
|
|
3795
3877
|
const validation = FindAndReplaceInDocSchema.safeParse(args);
|
|
3796
3878
|
if (!validation.success) {
|
|
@@ -3999,11 +4081,13 @@ ${index + 1}. ${replyAuthor} (${replyDate})
|
|
|
3999
4081
|
commentId: a.commentId,
|
|
4000
4082
|
fields: "id,content,author,createdTime",
|
|
4001
4083
|
requestBody: {
|
|
4002
|
-
content: a.replyText
|
|
4084
|
+
content: a.replyText,
|
|
4085
|
+
...a.resolve && { action: "resolve" }
|
|
4003
4086
|
}
|
|
4004
4087
|
});
|
|
4088
|
+
const resolveNote = a.resolve ? " Comment thread resolved." : "";
|
|
4005
4089
|
return {
|
|
4006
|
-
content: [{ type: "text", text: `Reply added successfully. Reply ID: ${response.data.id}` }],
|
|
4090
|
+
content: [{ type: "text", text: `Reply added successfully. Reply ID: ${response.data.id}${resolveNote}` }],
|
|
4007
4091
|
isError: false
|
|
4008
4092
|
};
|
|
4009
4093
|
}
|
|
@@ -4758,7 +4842,21 @@ var toolDefinitions3 = [
|
|
|
4758
4842
|
properties: {
|
|
4759
4843
|
spreadsheetId: { type: "string", description: "The ID of the Google Spreadsheet (from the URL)" },
|
|
4760
4844
|
range: { type: "string", description: "A1 notation range indicating where to append (e.g., 'A1' or 'Sheet1!A1'). Data will be appended starting from this range." },
|
|
4761
|
-
values: {
|
|
4845
|
+
values: {
|
|
4846
|
+
type: "array",
|
|
4847
|
+
description: "2D array of values to append. Each inner array represents a row.",
|
|
4848
|
+
items: {
|
|
4849
|
+
type: "array",
|
|
4850
|
+
items: {
|
|
4851
|
+
anyOf: [
|
|
4852
|
+
{ type: "string" },
|
|
4853
|
+
{ type: "number" },
|
|
4854
|
+
{ type: "boolean" },
|
|
4855
|
+
{ type: "null" }
|
|
4856
|
+
]
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
},
|
|
4762
4860
|
valueInputOption: { type: "string", description: "How input data should be interpreted (RAW or USER_ENTERED)", enum: ["RAW", "USER_ENTERED"], default: "USER_ENTERED" }
|
|
4763
4861
|
},
|
|
4764
4862
|
required: ["spreadsheetId", "range", "values"]
|