@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 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 & Scope Presets (v1.7.0)
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, unlink as unlink2 } from "fs/promises";
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
- const escapedQuery = escapeDriveQuery(userQuery);
1407
- const formattedQuery = `fullText contains '${escapedQuery}' and trashed = false`;
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 fileList = res.data.files?.map((f) => `${f.name} (ID: ${f.id}, ${f.mimeType})`).join("\n") || "";
1418
- ctx.log("Search results", { query: userQuery, resultCount: res.data.files?.length });
1419
- let response = `Found ${res.data.files?.length ?? 0} files:
1420
- ${fileList}`;
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
- ctx.log("Uploading file", { localPath: data.localPath, name: fileName, mimeType: detectedMime, size: stats.size });
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: `'${escapeDriveQuery(data.folderId)}' in parents and mimeType='application/pdf' and trashed=false`,
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 || void 0, name: f.name || void 0, docId: converted.data.id || void 0, ok: true });
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 || void 0, name: f.name || void 0, ok: false, error: message });
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.split(/[\\/]/).pop() || "upload.pdf";
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 requestedScopes;
2157
+ let scopeStatus;
2118
2158
  try {
2119
- requestedScopes = resolveOAuthScopes();
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 = getGrantedScopesFromAuthClient(ctx);
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 requestedScopes;
2191
+ let scopeStatus;
2153
2192
  try {
2154
- requestedScopes = resolveOAuthScopes();
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 = getGrantedScopesFromAuthClient(ctx);
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: { type: "array", description: "2D array of values to append. Each inner array represents a row.", items: { type: "array" } },
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"]