@piotr-agier/google-drive-mcp 2.0.2 → 2.2.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/dist/index.js CHANGED
@@ -427,7 +427,14 @@ var AuthServer = class {
427
427
  this.baseOAuth2Client = oauth2Client;
428
428
  this.tokenManager = new TokenManager(oauth2Client);
429
429
  this.app = express();
430
- this.portRange = { start: 3e3, end: 3004 };
430
+ const raw = process.env.GOOGLE_DRIVE_MCP_AUTH_PORT;
431
+ const portStart = raw ? Number(raw) : 3e3;
432
+ if (!Number.isInteger(portStart) || portStart < 1 || portStart > 65531) {
433
+ throw new Error(
434
+ `Invalid GOOGLE_DRIVE_MCP_AUTH_PORT: "${raw}". Must be an integer between 1 and 65531.`
435
+ );
436
+ }
437
+ this.portRange = { start: portStart, end: portStart + 4 };
431
438
  this.setupRoutes();
432
439
  }
433
440
  setupRoutes() {
@@ -1085,7 +1092,8 @@ var AddPermissionSchema = z.object({
1085
1092
  emailAddress: z.string().email("Valid email is required"),
1086
1093
  role: z.enum(["owner", "organizer", "fileOrganizer", "writer", "commenter", "reader"]).default("reader"),
1087
1094
  type: z.enum(["user", "group", "domain", "anyone"]).default("user"),
1088
- sendNotificationEmail: z.boolean().optional().default(false)
1095
+ sendNotificationEmail: z.boolean().optional().default(false),
1096
+ emailMessage: z.string().optional()
1089
1097
  });
1090
1098
  var UpdatePermissionSchema = z.object({
1091
1099
  fileId: z.string().min(1, "File ID is required"),
@@ -1105,7 +1113,8 @@ var ShareFileSchema = z.object({
1105
1113
  fileId: z.string().min(1, "File ID is required"),
1106
1114
  emailAddress: z.string().email("Valid email is required"),
1107
1115
  role: z.enum(["writer", "commenter", "reader"]).default("reader"),
1108
- sendNotificationEmail: z.boolean().optional().default(true)
1116
+ sendNotificationEmail: z.boolean().optional().default(true),
1117
+ emailMessage: z.string().optional()
1109
1118
  });
1110
1119
  var ConvertPdfToGoogleDocSchema = z.object({
1111
1120
  fileId: z.string().min(1, "File ID is required"),
@@ -1349,7 +1358,8 @@ var toolDefinitions = [
1349
1358
  emailAddress: { type: "string", description: "Target user/group email" },
1350
1359
  role: { type: "string", enum: ["owner", "organizer", "fileOrganizer", "writer", "commenter", "reader"], description: "Permission role" },
1351
1360
  type: { type: "string", enum: ["user", "group", "domain", "anyone"], description: "Principal type" },
1352
- sendNotificationEmail: { type: "boolean", description: "Send notification email" }
1361
+ sendNotificationEmail: { type: "boolean", description: "Send notification email" },
1362
+ emailMessage: { type: "string", description: "Custom message to include in the notification email. Ignored unless sendNotificationEmail is true." }
1353
1363
  },
1354
1364
  required: ["fileId", "emailAddress"]
1355
1365
  }
@@ -1389,7 +1399,8 @@ var toolDefinitions = [
1389
1399
  fileId: { type: "string", description: "Google Drive file ID" },
1390
1400
  emailAddress: { type: "string", description: "User email" },
1391
1401
  role: { type: "string", enum: ["writer", "commenter", "reader"], description: "Access role" },
1392
- sendNotificationEmail: { type: "boolean", description: "Send notification email" }
1402
+ sendNotificationEmail: { type: "boolean", description: "Send notification email" },
1403
+ emailMessage: { type: "string", description: "Custom message to include in the notification email. Ignored unless sendNotificationEmail is true." }
1393
1404
  },
1394
1405
  required: ["fileId", "emailAddress"]
1395
1406
  }
@@ -2139,6 +2150,7 @@ ${lines.join("\n")}` }], isError: false };
2139
2150
  emailAddress: data.emailAddress
2140
2151
  },
2141
2152
  sendNotificationEmail: data.sendNotificationEmail,
2153
+ ...data.emailMessage && { emailMessage: data.emailMessage },
2142
2154
  fields: "id,type,role,emailAddress",
2143
2155
  supportsAllDrives: true
2144
2156
  });
@@ -2225,6 +2237,7 @@ ${lines.join("\n")}` }], isError: false };
2225
2237
  emailAddress: data.emailAddress
2226
2238
  },
2227
2239
  sendNotificationEmail: data.sendNotificationEmail,
2240
+ ...data.emailMessage && { emailMessage: data.emailMessage },
2228
2241
  fields: "id,type,role,emailAddress",
2229
2242
  supportsAllDrives: true
2230
2243
  });
@@ -2555,9 +2568,62 @@ __export(docs_exports, {
2555
2568
  toolDefinitions: () => toolDefinitions2
2556
2569
  });
2557
2570
  import { z as z2 } from "zod";
2558
- import { createReadStream as createReadStream2, existsSync as existsSync3 } from "fs";
2559
- import { basename as basename3, extname as extname3 } from "path";
2560
2571
  import JSZip from "jszip";
2572
+
2573
+ // src/utils/driveImageUpload.ts
2574
+ import { existsSync as existsSync3, createReadStream as createReadStream2 } from "fs";
2575
+ import { basename as basename3, extname as extname3 } from "path";
2576
+ var MIME_BY_EXT = {
2577
+ ".jpg": "image/jpeg",
2578
+ ".jpeg": "image/jpeg",
2579
+ ".png": "image/png",
2580
+ ".gif": "image/gif",
2581
+ ".bmp": "image/bmp",
2582
+ ".webp": "image/webp",
2583
+ ".svg": "image/svg+xml"
2584
+ };
2585
+ async function uploadImageToDrive(ctx, localFilePath, options = {}) {
2586
+ const { parentFolderId, makePublic = false } = options;
2587
+ if (!existsSync3(localFilePath)) {
2588
+ throw new Error(`Image file not found: ${localFilePath}`);
2589
+ }
2590
+ const fileName = basename3(localFilePath);
2591
+ const ext = extname3(localFilePath).toLowerCase();
2592
+ const mimeType = MIME_BY_EXT[ext] || "application/octet-stream";
2593
+ const requestBody = {
2594
+ name: fileName,
2595
+ mimeType
2596
+ };
2597
+ if (parentFolderId) requestBody.parents = [parentFolderId];
2598
+ const drive = ctx.getDrive();
2599
+ const uploadResponse = await drive.files.create({
2600
+ requestBody,
2601
+ media: { mimeType, body: createReadStream2(localFilePath) },
2602
+ fields: "id,webViewLink,webContentLink",
2603
+ supportsAllDrives: true
2604
+ });
2605
+ const fileId = uploadResponse.data.id;
2606
+ if (!fileId) throw new Error("Failed to upload image to Drive - no file ID returned");
2607
+ if (makePublic) {
2608
+ await drive.permissions.create({
2609
+ fileId,
2610
+ requestBody: { role: "reader", type: "anyone" }
2611
+ });
2612
+ }
2613
+ const fileInfo = await drive.files.get({
2614
+ fileId,
2615
+ fields: "webContentLink",
2616
+ supportsAllDrives: true
2617
+ });
2618
+ const webContentLink = fileInfo.data.webContentLink;
2619
+ if (!webContentLink) throw new Error("Failed to get web content link for uploaded image");
2620
+ return { fileId, webContentLink };
2621
+ }
2622
+ async function deleteDriveFile(ctx, fileId) {
2623
+ await ctx.getDrive().files.delete({ fileId, supportsAllDrives: true });
2624
+ }
2625
+
2626
+ // src/tools/docs.ts
2561
2627
  function hexToRgbColor(hex) {
2562
2628
  if (!hex) return null;
2563
2629
  let hexClean = hex.startsWith("#") ? hex.slice(1) : hex;
@@ -2853,64 +2919,6 @@ async function insertInlineImageHelper(ctx, documentId, imageUrl, index, width,
2853
2919
  }
2854
2920
  return executeBatchUpdate(ctx, documentId, [request]);
2855
2921
  }
2856
- async function uploadImageToDriveHelper(ctx, localFilePath, parentFolderId, makePublic = false) {
2857
- if (!existsSync3(localFilePath)) {
2858
- throw new Error(`Image file not found: ${localFilePath}`);
2859
- }
2860
- const fileName = basename3(localFilePath);
2861
- const mimeTypeMap = {
2862
- ".jpg": "image/jpeg",
2863
- ".jpeg": "image/jpeg",
2864
- ".png": "image/png",
2865
- ".gif": "image/gif",
2866
- ".bmp": "image/bmp",
2867
- ".webp": "image/webp",
2868
- ".svg": "image/svg+xml"
2869
- };
2870
- const ext = extname3(localFilePath).toLowerCase();
2871
- const mimeType = mimeTypeMap[ext] || "application/octet-stream";
2872
- const fileMetadata = {
2873
- name: fileName,
2874
- mimeType
2875
- };
2876
- if (parentFolderId) {
2877
- fileMetadata.parents = [parentFolderId];
2878
- }
2879
- const media = {
2880
- mimeType,
2881
- body: createReadStream2(localFilePath)
2882
- };
2883
- const drive = ctx.getDrive();
2884
- const uploadResponse = await drive.files.create({
2885
- requestBody: fileMetadata,
2886
- media,
2887
- fields: "id,webViewLink,webContentLink",
2888
- supportsAllDrives: true
2889
- });
2890
- const fileId = uploadResponse.data.id;
2891
- if (!fileId) {
2892
- throw new Error("Failed to upload image to Drive - no file ID returned");
2893
- }
2894
- if (makePublic) {
2895
- await drive.permissions.create({
2896
- fileId,
2897
- requestBody: {
2898
- role: "reader",
2899
- type: "anyone"
2900
- }
2901
- });
2902
- }
2903
- const fileInfo = await drive.files.get({
2904
- fileId,
2905
- fields: "webContentLink",
2906
- supportsAllDrives: true
2907
- });
2908
- const webContentLink = fileInfo.data.webContentLink;
2909
- if (!webContentLink) {
2910
- throw new Error("Failed to get web content link for uploaded image");
2911
- }
2912
- return webContentLink;
2913
- }
2914
2922
  var MAX_ROW_XML_DISTANCE = 1e5;
2915
2923
  var MAX_PARAGRAPH_XML_DISTANCE = 5e4;
2916
2924
  var MAX_PARAGRAPH_CONTEXT_LENGTH = 300;
@@ -3148,7 +3156,8 @@ var CreateGoogleDocSchema = z2.object({
3148
3156
  });
3149
3157
  var UpdateGoogleDocSchema = z2.object({
3150
3158
  documentId: z2.string().min(1, "Document ID is required"),
3151
- content: z2.string()
3159
+ content: z2.string(),
3160
+ tabId: z2.string().optional()
3152
3161
  });
3153
3162
  var GetGoogleDocContentSchema = z2.object({
3154
3163
  documentId: z2.string().min(1, "Document ID is required"),
@@ -3157,12 +3166,14 @@ var GetGoogleDocContentSchema = z2.object({
3157
3166
  var InsertTextSchema = z2.object({
3158
3167
  documentId: z2.string().min(1, "Document ID is required"),
3159
3168
  text: z2.string().min(1, "Text to insert is required"),
3160
- index: z2.number().int().min(1, "Index must be at least 1 (1-based)")
3169
+ index: z2.number().int().min(1, "Index must be at least 1 (1-based)"),
3170
+ tabId: z2.string().optional()
3161
3171
  });
3162
3172
  var DeleteRangeSchema = z2.object({
3163
3173
  documentId: z2.string().min(1, "Document ID is required"),
3164
3174
  startIndex: z2.number().int().min(1, "Start index must be at least 1"),
3165
- endIndex: z2.number().int().min(1, "End index must be at least 1")
3175
+ endIndex: z2.number().int().min(1, "End index must be at least 1"),
3176
+ tabId: z2.string().optional()
3166
3177
  }).refine((data) => data.endIndex > data.startIndex, {
3167
3178
  message: "End index must be greater than start index",
3168
3179
  path: ["endIndex"]
@@ -3303,7 +3314,8 @@ var FindAndReplaceInDocSchema = z2.object({
3303
3314
  findText: z2.string().min(1, "findText is required"),
3304
3315
  replaceText: z2.string(),
3305
3316
  matchCase: z2.boolean().optional().default(false),
3306
- dryRun: z2.boolean().optional().default(false)
3317
+ dryRun: z2.boolean().optional().default(false),
3318
+ tabId: z2.string().optional()
3307
3319
  });
3308
3320
  var AddDocumentTabSchema = z2.object({
3309
3321
  documentId: z2.string().min(1, "Document ID is required"),
@@ -3347,38 +3359,41 @@ var toolDefinitions2 = [
3347
3359
  },
3348
3360
  {
3349
3361
  name: "updateGoogleDoc",
3350
- description: "Update an existing Google Doc",
3362
+ description: "Update an existing Google Doc (replaces all content). For multi-tab docs, specify tabId to replace a single tab's content atomically; leaves other tabs untouched.",
3351
3363
  inputSchema: {
3352
3364
  type: "object",
3353
3365
  properties: {
3354
3366
  documentId: { type: "string", description: "Doc ID" },
3355
- content: { type: "string", description: "New content" }
3367
+ content: { type: "string", description: "New content" },
3368
+ tabId: { type: "string", description: "Optional. Tab ID to replace (from listDocumentTabs). If set, delete+insert run in a single atomic batchUpdate scoped to that tab." }
3356
3369
  },
3357
3370
  required: ["documentId", "content"]
3358
3371
  }
3359
3372
  },
3360
3373
  {
3361
3374
  name: "insertText",
3362
- description: "Insert text at a specific index in a Google Doc (surgical edit, doesn't replace entire doc)",
3375
+ description: "Insert text at a specific index in a Google Doc (surgical edit, doesn't replace entire doc). For multi-tab docs, specify tabId to target a specific tab.",
3363
3376
  inputSchema: {
3364
3377
  type: "object",
3365
3378
  properties: {
3366
3379
  documentId: { type: "string", description: "The document ID" },
3367
3380
  text: { type: "string", description: "Text to insert" },
3368
- index: { type: "number", description: "Position to insert at (1-based)" }
3381
+ index: { type: "number", description: "Position to insert at (1-based)" },
3382
+ tabId: { type: "string", description: "Optional. Tab ID to insert into (from listDocumentTabs). If omitted, inserts into the first/default tab." }
3369
3383
  },
3370
3384
  required: ["documentId", "text", "index"]
3371
3385
  }
3372
3386
  },
3373
3387
  {
3374
3388
  name: "deleteRange",
3375
- description: "Delete content between start and end indices in a Google Doc",
3389
+ description: "Delete content between start and end indices in a Google Doc. For multi-tab docs, specify tabId to target a specific tab.",
3376
3390
  inputSchema: {
3377
3391
  type: "object",
3378
3392
  properties: {
3379
3393
  documentId: { type: "string", description: "The document ID" },
3380
3394
  startIndex: { type: "number", description: "Start index (1-based, inclusive)" },
3381
- endIndex: { type: "number", description: "End index (exclusive)" }
3395
+ endIndex: { type: "number", description: "End index (exclusive)" },
3396
+ tabId: { type: "string", description: "Optional. Tab ID to delete from (from listDocumentTabs). If omitted, deletes from the first/default tab." }
3382
3397
  },
3383
3398
  required: ["documentId", "startIndex", "endIndex"]
3384
3399
  }
@@ -3521,7 +3536,7 @@ var toolDefinitions2 = [
3521
3536
  },
3522
3537
  {
3523
3538
  name: "findAndReplaceInDoc",
3524
- 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.)",
3539
+ 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.). For multi-tab docs, specify tabId to scope replacements to a single tab.",
3525
3540
  inputSchema: {
3526
3541
  type: "object",
3527
3542
  properties: {
@@ -3529,7 +3544,8 @@ var toolDefinitions2 = [
3529
3544
  findText: { type: "string", description: "Text to find" },
3530
3545
  replaceText: { type: "string", description: "Replacement text" },
3531
3546
  matchCase: { type: "boolean", description: "Case-sensitive match (default: false)" },
3532
- dryRun: { type: "boolean", description: "Only count approximate matches from paragraph text, do not modify document (default: false)" }
3547
+ dryRun: { type: "boolean", description: "Only count approximate matches from paragraph text, do not modify document (default: false). Ignores tabId \u2014 always scans the full document body." },
3548
+ tabId: { type: "string", description: "Optional. Tab ID to scope replacements to (from listDocumentTabs). If omitted, replaces across all tabs." }
3533
3549
  },
3534
3550
  required: ["documentId", "findText", "replaceText"]
3535
3551
  }
@@ -3843,6 +3859,43 @@ Link: ${doc.webViewLink}` }],
3843
3859
  }
3844
3860
  const a = validation.data;
3845
3861
  const docs = ctx.google.docs({ version: "v1", auth: ctx.authClient });
3862
+ if (a.tabId) {
3863
+ const document2 = await docs.documents.get({ documentId: a.documentId, includeTabsContent: true });
3864
+ const tabs = document2.data.tabs;
3865
+ const tab = tabs ? findTabById(tabs, a.tabId) : null;
3866
+ if (!tab) {
3867
+ return errorResponse(`Tab with ID "${a.tabId}" not found. Use listDocumentTabs to see available tabs.`);
3868
+ }
3869
+ const bodyContent = tab.documentTab?.body?.content;
3870
+ const lastEndIndex = bodyContent?.[bodyContent.length - 1]?.endIndex ?? 1;
3871
+ const deleteEndIndex2 = Math.max(1, lastEndIndex - 1);
3872
+ const requests = [];
3873
+ if (deleteEndIndex2 > 1) {
3874
+ requests.push({
3875
+ deleteContentRange: {
3876
+ range: { startIndex: 1, endIndex: deleteEndIndex2, tabId: a.tabId }
3877
+ }
3878
+ });
3879
+ }
3880
+ requests.push({
3881
+ insertText: { location: { index: 1, tabId: a.tabId }, text: a.content }
3882
+ });
3883
+ requests.push({
3884
+ updateParagraphStyle: {
3885
+ range: { startIndex: 1, endIndex: a.content.length + 1, tabId: a.tabId },
3886
+ paragraphStyle: { namedStyleType: "NORMAL_TEXT" },
3887
+ fields: "namedStyleType"
3888
+ }
3889
+ });
3890
+ await docs.documents.batchUpdate({
3891
+ documentId: a.documentId,
3892
+ requestBody: { requests }
3893
+ });
3894
+ return {
3895
+ content: [{ type: "text", text: `Updated Google Doc: ${document2.data.title} (tab: ${a.tabId})` }],
3896
+ isError: false
3897
+ };
3898
+ }
3846
3899
  const document = await docs.documents.get({ documentId: a.documentId });
3847
3900
  const endIndex = document.data.body?.content?.[document.data.body.content.length - 1]?.endIndex || 1;
3848
3901
  const deleteEndIndex = Math.max(1, endIndex - 1);
@@ -4123,20 +4176,22 @@ Total length: ${totalLength} characters`
4123
4176
  return errorResponse(validation.error.errors[0].message);
4124
4177
  }
4125
4178
  const a = validation.data;
4179
+ const location = { index: a.index };
4180
+ if (a.tabId) location.tabId = a.tabId;
4126
4181
  const docs = ctx.google.docs({ version: "v1", auth: ctx.authClient });
4127
4182
  await docs.documents.batchUpdate({
4128
4183
  documentId: a.documentId,
4129
4184
  requestBody: {
4130
4185
  requests: [{
4131
4186
  insertText: {
4132
- location: { index: a.index },
4187
+ location,
4133
4188
  text: a.text
4134
4189
  }
4135
4190
  }]
4136
4191
  }
4137
4192
  });
4138
4193
  return {
4139
- content: [{ type: "text", text: `Successfully inserted ${a.text.length} characters at index ${a.index}` }],
4194
+ content: [{ type: "text", text: `Successfully inserted ${a.text.length} characters at index ${a.index}${a.tabId ? ` in tab ${a.tabId}` : ""}` }],
4140
4195
  isError: false
4141
4196
  };
4142
4197
  }
@@ -4149,22 +4204,22 @@ Total length: ${totalLength} characters`
4149
4204
  if (a.endIndex <= a.startIndex) {
4150
4205
  return errorResponse("endIndex must be greater than startIndex");
4151
4206
  }
4207
+ const range = {
4208
+ startIndex: a.startIndex,
4209
+ endIndex: a.endIndex
4210
+ };
4211
+ if (a.tabId) range.tabId = a.tabId;
4152
4212
  const docs = ctx.google.docs({ version: "v1", auth: ctx.authClient });
4153
4213
  await docs.documents.batchUpdate({
4154
4214
  documentId: a.documentId,
4155
4215
  requestBody: {
4156
4216
  requests: [{
4157
- deleteContentRange: {
4158
- range: {
4159
- startIndex: a.startIndex,
4160
- endIndex: a.endIndex
4161
- }
4162
- }
4217
+ deleteContentRange: { range }
4163
4218
  }]
4164
4219
  }
4165
4220
  });
4166
4221
  return {
4167
- content: [{ type: "text", text: `Successfully deleted content from index ${a.startIndex} to ${a.endIndex}` }],
4222
+ content: [{ type: "text", text: `Successfully deleted content from index ${a.startIndex} to ${a.endIndex}${a.tabId ? ` in tab ${a.tabId}` : ""}` }],
4168
4223
  isError: false
4169
4224
  };
4170
4225
  }
@@ -4546,25 +4601,20 @@ ${text}`;
4546
4601
  isError: false
4547
4602
  };
4548
4603
  }
4604
+ const replaceAllText = {
4605
+ containsText: { text: a.findText, matchCase: a.matchCase },
4606
+ replaceText: a.replaceText
4607
+ };
4608
+ if (a.tabId) replaceAllText.tabsCriteria = { tabIds: [a.tabId] };
4549
4609
  const response = await docs.documents.batchUpdate({
4550
4610
  documentId: a.documentId,
4551
4611
  requestBody: {
4552
- requests: [
4553
- {
4554
- replaceAllText: {
4555
- containsText: {
4556
- text: a.findText,
4557
- matchCase: a.matchCase
4558
- },
4559
- replaceText: a.replaceText
4560
- }
4561
- }
4562
- ]
4612
+ requests: [{ replaceAllText }]
4563
4613
  }
4564
4614
  });
4565
4615
  const occurrences = response.data.replies?.[0]?.replaceAllText?.occurrencesChanged ?? 0;
4566
4616
  return {
4567
- content: [{ type: "text", text: `Replaced ${occurrences} occurrence(s) of "${a.findText}".` }],
4617
+ content: [{ type: "text", text: `Replaced ${occurrences} occurrence(s) of "${a.findText}"${a.tabId ? ` in tab ${a.tabId}` : ""}.` }],
4568
4618
  isError: false
4569
4619
  };
4570
4620
  }
@@ -5008,7 +5058,10 @@ ${index + 1}. ${replyAuthor} (${replyDate})
5008
5058
  });
5009
5059
  parentFolderId = fileInfo.data.parents?.[0];
5010
5060
  }
5011
- const imageUrl = await uploadImageToDriveHelper(ctx, a.localImagePath, parentFolderId, a.makePublic);
5061
+ const { webContentLink: imageUrl } = await uploadImageToDrive(ctx, a.localImagePath, {
5062
+ parentFolderId,
5063
+ makePublic: a.makePublic
5064
+ });
5012
5065
  await insertInlineImageHelper(ctx, a.documentId, imageUrl, a.index, a.width, a.height);
5013
5066
  return {
5014
5067
  content: [{ type: "text", text: `Successfully uploaded and inserted local image at index ${a.index}
@@ -5133,8 +5186,10 @@ Image URL: ${imageUrl}` }],
5133
5186
  const docs = ctx.google.docs({ version: "v1", auth: ctx.authClient });
5134
5187
  await docs.documents.batchUpdate({
5135
5188
  documentId: a.documentId,
5136
- // updateDocumentTabProperties is not yet in the googleapis TypeScript types — cast required
5137
- requestBody: { requests: [{ updateDocumentTabProperties: { tabId: a.tabId, tabProperties: { title: a.title }, fields: "title" } }] }
5189
+ // updateDocumentTabProperties is not yet in the googleapis TypeScript types — cast required.
5190
+ // Per Google Docs API spec: tabId lives INSIDE tabProperties (it's the tab identifier),
5191
+ // and `fields` is a FieldMask for which properties to update (excludes tabId).
5192
+ requestBody: { requests: [{ updateDocumentTabProperties: { tabProperties: { tabId: a.tabId, title: a.title }, fields: "title" } }] }
5138
5193
  });
5139
5194
  return { content: [{ type: "text", text: `Renamed tab ${a.tabId} to "${a.title}".` }], isError: false };
5140
5195
  }
@@ -6629,6 +6684,72 @@ var ExportSlideThumbnailSchema = z4.object({
6629
6684
  mimeType: z4.enum(["PNG", "JPEG"]).optional().default("PNG"),
6630
6685
  size: z4.enum(["SMALL", "MEDIUM", "LARGE"]).optional().default("LARGE")
6631
6686
  });
6687
+ var InsertSlidesImageFromUrlSchema = z4.object({
6688
+ presentationId: z4.string().min(1, "Presentation ID is required"),
6689
+ pageObjectId: z4.string().min(1, "Slide/page object ID is required"),
6690
+ imageUrl: z4.string().url("A valid image URL is required"),
6691
+ x: z4.number().optional().default(0),
6692
+ y: z4.number().optional().default(0),
6693
+ width: z4.number().optional(),
6694
+ height: z4.number().optional()
6695
+ });
6696
+ var MoveSlideElementSchema = z4.object({
6697
+ presentationId: z4.string().min(1, "Presentation ID is required"),
6698
+ objectId: z4.string().min(1, "Element object ID is required"),
6699
+ x: z4.number().optional(),
6700
+ y: z4.number().optional(),
6701
+ width: z4.number().optional(),
6702
+ height: z4.number().optional()
6703
+ });
6704
+ var DeleteSlideElementSchema = z4.object({
6705
+ presentationId: z4.string().min(1, "Presentation ID is required"),
6706
+ objectId: z4.string().min(1, "Element object ID is required")
6707
+ });
6708
+ var GetSlideElementInfoSchema = z4.object({
6709
+ presentationId: z4.string().min(1, "Presentation ID is required"),
6710
+ slideObjectId: z4.string().optional()
6711
+ });
6712
+ var InsertSlidesLocalImageSchema = z4.object({
6713
+ presentationId: z4.string().min(1, "Presentation ID is required"),
6714
+ pageObjectId: z4.string().min(1, "Slide/page object ID is required"),
6715
+ localImagePath: z4.string().min(1, "Local image path is required"),
6716
+ x: z4.number().optional().default(0),
6717
+ y: z4.number().optional().default(0),
6718
+ width: z4.number().optional(),
6719
+ height: z4.number().optional()
6720
+ });
6721
+ async function insertImageIntoSlide(ctx, presentationId, pageObjectId, imageUrl, x, y, width, height) {
6722
+ const slidesService = ctx.google.slides({ version: "v1", auth: ctx.authClient });
6723
+ const objectId = `img_${uuidv4().substring(0, 8)}`;
6724
+ const elementProperties = {
6725
+ pageObjectId,
6726
+ transform: {
6727
+ scaleX: 1,
6728
+ scaleY: 1,
6729
+ translateX: x,
6730
+ translateY: y,
6731
+ unit: "EMU"
6732
+ }
6733
+ };
6734
+ if (width != null && height != null) {
6735
+ elementProperties.size = {
6736
+ width: { magnitude: width, unit: "EMU" },
6737
+ height: { magnitude: height, unit: "EMU" }
6738
+ };
6739
+ }
6740
+ await slidesService.presentations.batchUpdate({
6741
+ presentationId,
6742
+ requestBody: {
6743
+ requests: [{
6744
+ createImage: { objectId, url: imageUrl, elementProperties }
6745
+ }]
6746
+ }
6747
+ });
6748
+ return {
6749
+ content: [{ type: "text", text: `Inserted image into slide ${pageObjectId} (objectId: ${objectId})` }],
6750
+ isError: false
6751
+ };
6752
+ }
6632
6753
  var toolDefinitions4 = [
6633
6754
  {
6634
6755
  name: "createGoogleSlides",
@@ -6942,6 +7063,80 @@ var toolDefinitions4 = [
6942
7063
  },
6943
7064
  required: ["presentationId", "slideObjectId"]
6944
7065
  }
7066
+ },
7067
+ {
7068
+ name: "insertSlidesImageFromUrl",
7069
+ description: "Insert an image into a Google Slides slide from a publicly accessible URL",
7070
+ inputSchema: {
7071
+ type: "object",
7072
+ properties: {
7073
+ presentationId: { type: "string", description: "Presentation ID" },
7074
+ pageObjectId: { type: "string", description: "Slide/page object ID to insert the image into" },
7075
+ imageUrl: { type: "string", description: "Publicly accessible URL of the image" },
7076
+ x: { type: "number", description: "X position in EMU (default: 0)" },
7077
+ y: { type: "number", description: "Y position in EMU (default: 0)" },
7078
+ width: { type: "number", description: "Width in EMU (omit to auto-size)" },
7079
+ height: { type: "number", description: "Height in EMU (omit to auto-size)" }
7080
+ },
7081
+ required: ["presentationId", "pageObjectId", "imageUrl"]
7082
+ }
7083
+ },
7084
+ {
7085
+ name: "getSlideElementInfo",
7086
+ description: "Get position, size, and transform of all elements on a slide. Returns actual rendered bounds.",
7087
+ inputSchema: {
7088
+ type: "object",
7089
+ properties: {
7090
+ presentationId: { type: "string", description: "Presentation ID" },
7091
+ slideObjectId: { type: "string", description: "Slide object ID (omit to get all slides)" }
7092
+ },
7093
+ required: ["presentationId"]
7094
+ }
7095
+ },
7096
+ {
7097
+ name: "moveSlideElement",
7098
+ description: "Move and/or resize an element (image, text box, shape) on a Google Slides slide",
7099
+ inputSchema: {
7100
+ type: "object",
7101
+ properties: {
7102
+ presentationId: { type: "string", description: "Presentation ID" },
7103
+ objectId: { type: "string", description: "Element object ID to move/resize" },
7104
+ x: { type: "number", description: "New X position in EMU" },
7105
+ y: { type: "number", description: "New Y position in EMU" },
7106
+ width: { type: "number", description: "New width in EMU" },
7107
+ height: { type: "number", description: "New height in EMU" }
7108
+ },
7109
+ required: ["presentationId", "objectId"]
7110
+ }
7111
+ },
7112
+ {
7113
+ name: "deleteSlideElement",
7114
+ description: "Delete an element (image, text box, shape) from a Google Slides slide",
7115
+ inputSchema: {
7116
+ type: "object",
7117
+ properties: {
7118
+ presentationId: { type: "string", description: "Presentation ID" },
7119
+ objectId: { type: "string", description: "Element object ID to delete" }
7120
+ },
7121
+ required: ["presentationId", "objectId"]
7122
+ }
7123
+ },
7124
+ {
7125
+ name: "insertSlidesLocalImage",
7126
+ description: "Upload a local image file to Google Drive and insert it into a Google Slides slide",
7127
+ inputSchema: {
7128
+ type: "object",
7129
+ properties: {
7130
+ presentationId: { type: "string", description: "Presentation ID" },
7131
+ pageObjectId: { type: "string", description: "Slide/page object ID to insert the image into" },
7132
+ localImagePath: { type: "string", description: "Absolute path to the local image file" },
7133
+ x: { type: "number", description: "X position in EMU (default: 0)" },
7134
+ y: { type: "number", description: "Y position in EMU (default: 0)" },
7135
+ width: { type: "number", description: "Width in EMU (omit to auto-size)" },
7136
+ height: { type: "number", description: "Height in EMU (omit to auto-size)" }
7137
+ },
7138
+ required: ["presentationId", "pageObjectId", "localImagePath"]
7139
+ }
6945
7140
  }
6946
7141
  ];
6947
7142
  async function handleTool4(toolName, args, ctx) {
@@ -7779,6 +7974,167 @@ Slide ${a.slideIndex ?? index} (ID: ${slide.objectId}):
7779
7974
  isError: false
7780
7975
  };
7781
7976
  }
7977
+ case "getSlideElementInfo": {
7978
+ const validation = GetSlideElementInfoSchema.safeParse(args);
7979
+ if (!validation.success) return errorResponse(validation.error.errors[0].message);
7980
+ const a = validation.data;
7981
+ const slidesService = ctx.google.slides({ version: "v1", auth: ctx.authClient });
7982
+ const sizeOnly = await slidesService.presentations.get({
7983
+ presentationId: a.presentationId,
7984
+ fields: "pageSize"
7985
+ });
7986
+ const slideWidth = sizeOnly.data.pageSize?.width?.magnitude || 9144e3;
7987
+ const slideHeight = sizeOnly.data.pageSize?.height?.magnitude || 6858e3;
7988
+ let slides = [];
7989
+ if (a.slideObjectId) {
7990
+ const page = await slidesService.presentations.pages.get({
7991
+ presentationId: a.presentationId,
7992
+ pageObjectId: a.slideObjectId,
7993
+ fields: "objectId,pageElements(objectId,transform,size,shape/shapeType,image)"
7994
+ });
7995
+ slides = [page.data];
7996
+ } else {
7997
+ const withSlides = await slidesService.presentations.get({
7998
+ presentationId: a.presentationId,
7999
+ fields: "slides(objectId,pageElements(objectId,transform,size,shape/shapeType,image))"
8000
+ });
8001
+ slides = withSlides.data.slides || [];
8002
+ }
8003
+ const lines = [`Slide dimensions: ${slideWidth} x ${slideHeight} EMU`];
8004
+ for (const slide of slides) {
8005
+ lines.push(`
8006
+ --- Slide: ${slide.objectId} ---`);
8007
+ for (const el of slide.pageElements || []) {
8008
+ const t = el.transform || {};
8009
+ const s = el.size || {};
8010
+ const intrW = s.width?.magnitude || 0;
8011
+ const intrH = s.height?.magnitude || 0;
8012
+ const scX = t.scaleX || 1;
8013
+ const scY = t.scaleY || 1;
8014
+ const tx = t.translateX || 0;
8015
+ const ty = t.translateY || 0;
8016
+ const renderedW = intrW * scX;
8017
+ const renderedH = intrH * scY;
8018
+ const right = tx + renderedW;
8019
+ const bottom = ty + renderedH;
8020
+ const offPage = tx < 0 || ty < 0 || right > slideWidth || bottom > slideHeight ? " *** OFF PAGE ***" : "";
8021
+ lines.push(` ${el.objectId} (${el.shape ? "shape:" + el.shape.shapeType : el.image ? "image" : "other"})`);
8022
+ lines.push(` intrinsic: ${intrW} x ${intrH}, scale: ${scX} x ${scY}`);
8023
+ lines.push(` rendered: ${Math.round(renderedW)} x ${Math.round(renderedH)} at (${tx}, ${ty})`);
8024
+ lines.push(` bounds: right=${Math.round(right)}, bottom=${Math.round(bottom)}${offPage}`);
8025
+ }
8026
+ }
8027
+ return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
8028
+ }
8029
+ case "moveSlideElement": {
8030
+ const validation = MoveSlideElementSchema.safeParse(args);
8031
+ if (!validation.success) return errorResponse(validation.error.errors[0].message);
8032
+ const a = validation.data;
8033
+ const slidesService = ctx.google.slides({ version: "v1", auth: ctx.authClient });
8034
+ const pres = await slidesService.presentations.get({
8035
+ presentationId: a.presentationId,
8036
+ fields: "slides(pageElements(objectId,transform,size))"
8037
+ });
8038
+ let currentTransform = null;
8039
+ let currentSize = null;
8040
+ for (const slide of pres.data.slides || []) {
8041
+ for (const el of slide.pageElements || []) {
8042
+ if (el.objectId === a.objectId) {
8043
+ currentTransform = el.transform || null;
8044
+ currentSize = el.size || null;
8045
+ break;
8046
+ }
8047
+ }
8048
+ if (currentTransform) break;
8049
+ }
8050
+ if (!currentTransform) {
8051
+ return errorResponse(`Element ${a.objectId} not found in presentation`);
8052
+ }
8053
+ const origWidth = currentSize?.width?.magnitude || 3e6;
8054
+ const origHeight = currentSize?.height?.magnitude || 3e6;
8055
+ const curScaleX = currentTransform.scaleX || 1;
8056
+ const curScaleY = currentTransform.scaleY || 1;
8057
+ const newScaleX = a.width !== void 0 ? a.width / origWidth : curScaleX;
8058
+ const newScaleY = a.height !== void 0 ? a.height / origHeight : curScaleY;
8059
+ const newX = a.x ?? (currentTransform.translateX || 0);
8060
+ const newY = a.y ?? (currentTransform.translateY || 0);
8061
+ const newTransform = {
8062
+ scaleX: newScaleX,
8063
+ scaleY: newScaleY,
8064
+ translateX: newX,
8065
+ translateY: newY,
8066
+ shearX: currentTransform.shearX || 0,
8067
+ shearY: currentTransform.shearY || 0,
8068
+ unit: "EMU"
8069
+ };
8070
+ const requests = [{
8071
+ updatePageElementTransform: {
8072
+ objectId: a.objectId,
8073
+ applyMode: "ABSOLUTE",
8074
+ transform: newTransform
8075
+ }
8076
+ }];
8077
+ await slidesService.presentations.batchUpdate({
8078
+ presentationId: a.presentationId,
8079
+ requestBody: { requests }
8080
+ });
8081
+ const didResize = a.width !== void 0 || a.height !== void 0;
8082
+ const action = didResize ? "Moved/resized" : "Moved";
8083
+ return {
8084
+ content: [{ type: "text", text: `${action} element ${a.objectId} to (${newX}, ${newY})` }],
8085
+ isError: false
8086
+ };
8087
+ }
8088
+ case "deleteSlideElement": {
8089
+ const validation = DeleteSlideElementSchema.safeParse(args);
8090
+ if (!validation.success) return errorResponse(validation.error.errors[0].message);
8091
+ const a = validation.data;
8092
+ const slidesService = ctx.google.slides({ version: "v1", auth: ctx.authClient });
8093
+ await slidesService.presentations.batchUpdate({
8094
+ presentationId: a.presentationId,
8095
+ requestBody: {
8096
+ requests: [{ deleteObject: { objectId: a.objectId } }]
8097
+ }
8098
+ });
8099
+ return {
8100
+ content: [{ type: "text", text: `Deleted element ${a.objectId}` }],
8101
+ isError: false
8102
+ };
8103
+ }
8104
+ case "insertSlidesImageFromUrl": {
8105
+ const validation = InsertSlidesImageFromUrlSchema.safeParse(args);
8106
+ if (!validation.success) return errorResponse(validation.error.errors[0].message);
8107
+ const a = validation.data;
8108
+ return insertImageIntoSlide(ctx, a.presentationId, a.pageObjectId, a.imageUrl, a.x, a.y, a.width, a.height);
8109
+ }
8110
+ case "insertSlidesLocalImage": {
8111
+ const validation = InsertSlidesLocalImageSchema.safeParse(args);
8112
+ if (!validation.success) return errorResponse(validation.error.errors[0].message);
8113
+ const a = validation.data;
8114
+ const { fileId, webContentLink } = await uploadImageToDrive(ctx, a.localImagePath, {
8115
+ makePublic: true
8116
+ });
8117
+ try {
8118
+ const result = await insertImageIntoSlide(
8119
+ ctx,
8120
+ a.presentationId,
8121
+ a.pageObjectId,
8122
+ webContentLink,
8123
+ a.x,
8124
+ a.y,
8125
+ a.width,
8126
+ a.height
8127
+ );
8128
+ await deleteDriveFile(ctx, fileId).catch(
8129
+ (err) => ctx.log(`insertSlidesLocalImage: failed to delete intermediary Drive file ${fileId}`, err)
8130
+ );
8131
+ return result;
8132
+ } catch (err) {
8133
+ await deleteDriveFile(ctx, fileId).catch(() => {
8134
+ });
8135
+ throw err;
8136
+ }
8137
+ }
7782
8138
  default:
7783
8139
  return null;
7784
8140
  }
@@ -8513,6 +8869,7 @@ Examples:
8513
8869
  Environment Variables:
8514
8870
  GOOGLE_DRIVE_OAUTH_CREDENTIALS Path to OAuth credentials file
8515
8871
  GOOGLE_DRIVE_MCP_TOKEN_PATH Path to store authentication tokens
8872
+ GOOGLE_DRIVE_MCP_AUTH_PORT Starting port for OAuth callback server (default: 3000, uses 5 consecutive ports)
8516
8873
 
8517
8874
  Transport Configuration:
8518
8875
  MCP_TRANSPORT Transport mode: stdio or http (default: stdio)
@@ -8538,8 +8895,9 @@ async function runAuthServer() {
8538
8895
  const authServerInstance = new AuthServer(oauth2Client);
8539
8896
  const success = await authServerInstance.start(true);
8540
8897
  if (!success && !authServerInstance.authCompletedSuccessfully) {
8898
+ const { start, end } = authServerInstance.portRange;
8541
8899
  console.error(
8542
- "Authentication failed. Could not start server or validate existing tokens. Check port availability (3000-3004) and try again."
8900
+ `Authentication failed. Could not start server or validate existing tokens. Check port availability (${start}-${end}) and try again.`
8543
8901
  );
8544
8902
  process.exit(1);
8545
8903
  } else if (authServerInstance.authCompletedSuccessfully) {