@piotr-agier/google-drive-mcp 2.0.2 → 2.1.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
@@ -2555,9 +2555,62 @@ __export(docs_exports, {
2555
2555
  toolDefinitions: () => toolDefinitions2
2556
2556
  });
2557
2557
  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
2558
  import JSZip from "jszip";
2559
+
2560
+ // src/utils/driveImageUpload.ts
2561
+ import { existsSync as existsSync3, createReadStream as createReadStream2 } from "fs";
2562
+ import { basename as basename3, extname as extname3 } from "path";
2563
+ var MIME_BY_EXT = {
2564
+ ".jpg": "image/jpeg",
2565
+ ".jpeg": "image/jpeg",
2566
+ ".png": "image/png",
2567
+ ".gif": "image/gif",
2568
+ ".bmp": "image/bmp",
2569
+ ".webp": "image/webp",
2570
+ ".svg": "image/svg+xml"
2571
+ };
2572
+ async function uploadImageToDrive(ctx, localFilePath, options = {}) {
2573
+ const { parentFolderId, makePublic = false } = options;
2574
+ if (!existsSync3(localFilePath)) {
2575
+ throw new Error(`Image file not found: ${localFilePath}`);
2576
+ }
2577
+ const fileName = basename3(localFilePath);
2578
+ const ext = extname3(localFilePath).toLowerCase();
2579
+ const mimeType = MIME_BY_EXT[ext] || "application/octet-stream";
2580
+ const requestBody = {
2581
+ name: fileName,
2582
+ mimeType
2583
+ };
2584
+ if (parentFolderId) requestBody.parents = [parentFolderId];
2585
+ const drive = ctx.getDrive();
2586
+ const uploadResponse = await drive.files.create({
2587
+ requestBody,
2588
+ media: { mimeType, body: createReadStream2(localFilePath) },
2589
+ fields: "id,webViewLink,webContentLink",
2590
+ supportsAllDrives: true
2591
+ });
2592
+ const fileId = uploadResponse.data.id;
2593
+ if (!fileId) throw new Error("Failed to upload image to Drive - no file ID returned");
2594
+ if (makePublic) {
2595
+ await drive.permissions.create({
2596
+ fileId,
2597
+ requestBody: { role: "reader", type: "anyone" }
2598
+ });
2599
+ }
2600
+ const fileInfo = await drive.files.get({
2601
+ fileId,
2602
+ fields: "webContentLink",
2603
+ supportsAllDrives: true
2604
+ });
2605
+ const webContentLink = fileInfo.data.webContentLink;
2606
+ if (!webContentLink) throw new Error("Failed to get web content link for uploaded image");
2607
+ return { fileId, webContentLink };
2608
+ }
2609
+ async function deleteDriveFile(ctx, fileId) {
2610
+ await ctx.getDrive().files.delete({ fileId, supportsAllDrives: true });
2611
+ }
2612
+
2613
+ // src/tools/docs.ts
2561
2614
  function hexToRgbColor(hex) {
2562
2615
  if (!hex) return null;
2563
2616
  let hexClean = hex.startsWith("#") ? hex.slice(1) : hex;
@@ -2853,64 +2906,6 @@ async function insertInlineImageHelper(ctx, documentId, imageUrl, index, width,
2853
2906
  }
2854
2907
  return executeBatchUpdate(ctx, documentId, [request]);
2855
2908
  }
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
2909
  var MAX_ROW_XML_DISTANCE = 1e5;
2915
2910
  var MAX_PARAGRAPH_XML_DISTANCE = 5e4;
2916
2911
  var MAX_PARAGRAPH_CONTEXT_LENGTH = 300;
@@ -5008,7 +5003,10 @@ ${index + 1}. ${replyAuthor} (${replyDate})
5008
5003
  });
5009
5004
  parentFolderId = fileInfo.data.parents?.[0];
5010
5005
  }
5011
- const imageUrl = await uploadImageToDriveHelper(ctx, a.localImagePath, parentFolderId, a.makePublic);
5006
+ const { webContentLink: imageUrl } = await uploadImageToDrive(ctx, a.localImagePath, {
5007
+ parentFolderId,
5008
+ makePublic: a.makePublic
5009
+ });
5012
5010
  await insertInlineImageHelper(ctx, a.documentId, imageUrl, a.index, a.width, a.height);
5013
5011
  return {
5014
5012
  content: [{ type: "text", text: `Successfully uploaded and inserted local image at index ${a.index}
@@ -6629,6 +6627,72 @@ var ExportSlideThumbnailSchema = z4.object({
6629
6627
  mimeType: z4.enum(["PNG", "JPEG"]).optional().default("PNG"),
6630
6628
  size: z4.enum(["SMALL", "MEDIUM", "LARGE"]).optional().default("LARGE")
6631
6629
  });
6630
+ var InsertSlidesImageFromUrlSchema = z4.object({
6631
+ presentationId: z4.string().min(1, "Presentation ID is required"),
6632
+ pageObjectId: z4.string().min(1, "Slide/page object ID is required"),
6633
+ imageUrl: z4.string().url("A valid image URL is required"),
6634
+ x: z4.number().optional().default(0),
6635
+ y: z4.number().optional().default(0),
6636
+ width: z4.number().optional(),
6637
+ height: z4.number().optional()
6638
+ });
6639
+ var MoveSlideElementSchema = z4.object({
6640
+ presentationId: z4.string().min(1, "Presentation ID is required"),
6641
+ objectId: z4.string().min(1, "Element object ID is required"),
6642
+ x: z4.number().optional(),
6643
+ y: z4.number().optional(),
6644
+ width: z4.number().optional(),
6645
+ height: z4.number().optional()
6646
+ });
6647
+ var DeleteSlideElementSchema = z4.object({
6648
+ presentationId: z4.string().min(1, "Presentation ID is required"),
6649
+ objectId: z4.string().min(1, "Element object ID is required")
6650
+ });
6651
+ var GetSlideElementInfoSchema = z4.object({
6652
+ presentationId: z4.string().min(1, "Presentation ID is required"),
6653
+ slideObjectId: z4.string().optional()
6654
+ });
6655
+ var InsertSlidesLocalImageSchema = z4.object({
6656
+ presentationId: z4.string().min(1, "Presentation ID is required"),
6657
+ pageObjectId: z4.string().min(1, "Slide/page object ID is required"),
6658
+ localImagePath: z4.string().min(1, "Local image path is required"),
6659
+ x: z4.number().optional().default(0),
6660
+ y: z4.number().optional().default(0),
6661
+ width: z4.number().optional(),
6662
+ height: z4.number().optional()
6663
+ });
6664
+ async function insertImageIntoSlide(ctx, presentationId, pageObjectId, imageUrl, x, y, width, height) {
6665
+ const slidesService = ctx.google.slides({ version: "v1", auth: ctx.authClient });
6666
+ const objectId = `img_${uuidv4().substring(0, 8)}`;
6667
+ const elementProperties = {
6668
+ pageObjectId,
6669
+ transform: {
6670
+ scaleX: 1,
6671
+ scaleY: 1,
6672
+ translateX: x,
6673
+ translateY: y,
6674
+ unit: "EMU"
6675
+ }
6676
+ };
6677
+ if (width != null && height != null) {
6678
+ elementProperties.size = {
6679
+ width: { magnitude: width, unit: "EMU" },
6680
+ height: { magnitude: height, unit: "EMU" }
6681
+ };
6682
+ }
6683
+ await slidesService.presentations.batchUpdate({
6684
+ presentationId,
6685
+ requestBody: {
6686
+ requests: [{
6687
+ createImage: { objectId, url: imageUrl, elementProperties }
6688
+ }]
6689
+ }
6690
+ });
6691
+ return {
6692
+ content: [{ type: "text", text: `Inserted image into slide ${pageObjectId} (objectId: ${objectId})` }],
6693
+ isError: false
6694
+ };
6695
+ }
6632
6696
  var toolDefinitions4 = [
6633
6697
  {
6634
6698
  name: "createGoogleSlides",
@@ -6942,6 +7006,80 @@ var toolDefinitions4 = [
6942
7006
  },
6943
7007
  required: ["presentationId", "slideObjectId"]
6944
7008
  }
7009
+ },
7010
+ {
7011
+ name: "insertSlidesImageFromUrl",
7012
+ description: "Insert an image into a Google Slides slide from a publicly accessible URL",
7013
+ inputSchema: {
7014
+ type: "object",
7015
+ properties: {
7016
+ presentationId: { type: "string", description: "Presentation ID" },
7017
+ pageObjectId: { type: "string", description: "Slide/page object ID to insert the image into" },
7018
+ imageUrl: { type: "string", description: "Publicly accessible URL of the image" },
7019
+ x: { type: "number", description: "X position in EMU (default: 0)" },
7020
+ y: { type: "number", description: "Y position in EMU (default: 0)" },
7021
+ width: { type: "number", description: "Width in EMU (omit to auto-size)" },
7022
+ height: { type: "number", description: "Height in EMU (omit to auto-size)" }
7023
+ },
7024
+ required: ["presentationId", "pageObjectId", "imageUrl"]
7025
+ }
7026
+ },
7027
+ {
7028
+ name: "getSlideElementInfo",
7029
+ description: "Get position, size, and transform of all elements on a slide. Returns actual rendered bounds.",
7030
+ inputSchema: {
7031
+ type: "object",
7032
+ properties: {
7033
+ presentationId: { type: "string", description: "Presentation ID" },
7034
+ slideObjectId: { type: "string", description: "Slide object ID (omit to get all slides)" }
7035
+ },
7036
+ required: ["presentationId"]
7037
+ }
7038
+ },
7039
+ {
7040
+ name: "moveSlideElement",
7041
+ description: "Move and/or resize an element (image, text box, shape) on a Google Slides slide",
7042
+ inputSchema: {
7043
+ type: "object",
7044
+ properties: {
7045
+ presentationId: { type: "string", description: "Presentation ID" },
7046
+ objectId: { type: "string", description: "Element object ID to move/resize" },
7047
+ x: { type: "number", description: "New X position in EMU" },
7048
+ y: { type: "number", description: "New Y position in EMU" },
7049
+ width: { type: "number", description: "New width in EMU" },
7050
+ height: { type: "number", description: "New height in EMU" }
7051
+ },
7052
+ required: ["presentationId", "objectId"]
7053
+ }
7054
+ },
7055
+ {
7056
+ name: "deleteSlideElement",
7057
+ description: "Delete an element (image, text box, shape) from a Google Slides slide",
7058
+ inputSchema: {
7059
+ type: "object",
7060
+ properties: {
7061
+ presentationId: { type: "string", description: "Presentation ID" },
7062
+ objectId: { type: "string", description: "Element object ID to delete" }
7063
+ },
7064
+ required: ["presentationId", "objectId"]
7065
+ }
7066
+ },
7067
+ {
7068
+ name: "insertSlidesLocalImage",
7069
+ description: "Upload a local image file to Google Drive and insert it into a Google Slides slide",
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
+ localImagePath: { type: "string", description: "Absolute path to the local image file" },
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", "localImagePath"]
7082
+ }
6945
7083
  }
6946
7084
  ];
6947
7085
  async function handleTool4(toolName, args, ctx) {
@@ -7779,6 +7917,167 @@ Slide ${a.slideIndex ?? index} (ID: ${slide.objectId}):
7779
7917
  isError: false
7780
7918
  };
7781
7919
  }
7920
+ case "getSlideElementInfo": {
7921
+ const validation = GetSlideElementInfoSchema.safeParse(args);
7922
+ if (!validation.success) return errorResponse(validation.error.errors[0].message);
7923
+ const a = validation.data;
7924
+ const slidesService = ctx.google.slides({ version: "v1", auth: ctx.authClient });
7925
+ const sizeOnly = await slidesService.presentations.get({
7926
+ presentationId: a.presentationId,
7927
+ fields: "pageSize"
7928
+ });
7929
+ const slideWidth = sizeOnly.data.pageSize?.width?.magnitude || 9144e3;
7930
+ const slideHeight = sizeOnly.data.pageSize?.height?.magnitude || 6858e3;
7931
+ let slides = [];
7932
+ if (a.slideObjectId) {
7933
+ const page = await slidesService.presentations.pages.get({
7934
+ presentationId: a.presentationId,
7935
+ pageObjectId: a.slideObjectId,
7936
+ fields: "objectId,pageElements(objectId,transform,size,shape/shapeType,image)"
7937
+ });
7938
+ slides = [page.data];
7939
+ } else {
7940
+ const withSlides = await slidesService.presentations.get({
7941
+ presentationId: a.presentationId,
7942
+ fields: "slides(objectId,pageElements(objectId,transform,size,shape/shapeType,image))"
7943
+ });
7944
+ slides = withSlides.data.slides || [];
7945
+ }
7946
+ const lines = [`Slide dimensions: ${slideWidth} x ${slideHeight} EMU`];
7947
+ for (const slide of slides) {
7948
+ lines.push(`
7949
+ --- Slide: ${slide.objectId} ---`);
7950
+ for (const el of slide.pageElements || []) {
7951
+ const t = el.transform || {};
7952
+ const s = el.size || {};
7953
+ const intrW = s.width?.magnitude || 0;
7954
+ const intrH = s.height?.magnitude || 0;
7955
+ const scX = t.scaleX || 1;
7956
+ const scY = t.scaleY || 1;
7957
+ const tx = t.translateX || 0;
7958
+ const ty = t.translateY || 0;
7959
+ const renderedW = intrW * scX;
7960
+ const renderedH = intrH * scY;
7961
+ const right = tx + renderedW;
7962
+ const bottom = ty + renderedH;
7963
+ const offPage = tx < 0 || ty < 0 || right > slideWidth || bottom > slideHeight ? " *** OFF PAGE ***" : "";
7964
+ lines.push(` ${el.objectId} (${el.shape ? "shape:" + el.shape.shapeType : el.image ? "image" : "other"})`);
7965
+ lines.push(` intrinsic: ${intrW} x ${intrH}, scale: ${scX} x ${scY}`);
7966
+ lines.push(` rendered: ${Math.round(renderedW)} x ${Math.round(renderedH)} at (${tx}, ${ty})`);
7967
+ lines.push(` bounds: right=${Math.round(right)}, bottom=${Math.round(bottom)}${offPage}`);
7968
+ }
7969
+ }
7970
+ return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
7971
+ }
7972
+ case "moveSlideElement": {
7973
+ const validation = MoveSlideElementSchema.safeParse(args);
7974
+ if (!validation.success) return errorResponse(validation.error.errors[0].message);
7975
+ const a = validation.data;
7976
+ const slidesService = ctx.google.slides({ version: "v1", auth: ctx.authClient });
7977
+ const pres = await slidesService.presentations.get({
7978
+ presentationId: a.presentationId,
7979
+ fields: "slides(pageElements(objectId,transform,size))"
7980
+ });
7981
+ let currentTransform = null;
7982
+ let currentSize = null;
7983
+ for (const slide of pres.data.slides || []) {
7984
+ for (const el of slide.pageElements || []) {
7985
+ if (el.objectId === a.objectId) {
7986
+ currentTransform = el.transform || null;
7987
+ currentSize = el.size || null;
7988
+ break;
7989
+ }
7990
+ }
7991
+ if (currentTransform) break;
7992
+ }
7993
+ if (!currentTransform) {
7994
+ return errorResponse(`Element ${a.objectId} not found in presentation`);
7995
+ }
7996
+ const origWidth = currentSize?.width?.magnitude || 3e6;
7997
+ const origHeight = currentSize?.height?.magnitude || 3e6;
7998
+ const curScaleX = currentTransform.scaleX || 1;
7999
+ const curScaleY = currentTransform.scaleY || 1;
8000
+ const newScaleX = a.width !== void 0 ? a.width / origWidth : curScaleX;
8001
+ const newScaleY = a.height !== void 0 ? a.height / origHeight : curScaleY;
8002
+ const newX = a.x ?? (currentTransform.translateX || 0);
8003
+ const newY = a.y ?? (currentTransform.translateY || 0);
8004
+ const newTransform = {
8005
+ scaleX: newScaleX,
8006
+ scaleY: newScaleY,
8007
+ translateX: newX,
8008
+ translateY: newY,
8009
+ shearX: currentTransform.shearX || 0,
8010
+ shearY: currentTransform.shearY || 0,
8011
+ unit: "EMU"
8012
+ };
8013
+ const requests = [{
8014
+ updatePageElementTransform: {
8015
+ objectId: a.objectId,
8016
+ applyMode: "ABSOLUTE",
8017
+ transform: newTransform
8018
+ }
8019
+ }];
8020
+ await slidesService.presentations.batchUpdate({
8021
+ presentationId: a.presentationId,
8022
+ requestBody: { requests }
8023
+ });
8024
+ const didResize = a.width !== void 0 || a.height !== void 0;
8025
+ const action = didResize ? "Moved/resized" : "Moved";
8026
+ return {
8027
+ content: [{ type: "text", text: `${action} element ${a.objectId} to (${newX}, ${newY})` }],
8028
+ isError: false
8029
+ };
8030
+ }
8031
+ case "deleteSlideElement": {
8032
+ const validation = DeleteSlideElementSchema.safeParse(args);
8033
+ if (!validation.success) return errorResponse(validation.error.errors[0].message);
8034
+ const a = validation.data;
8035
+ const slidesService = ctx.google.slides({ version: "v1", auth: ctx.authClient });
8036
+ await slidesService.presentations.batchUpdate({
8037
+ presentationId: a.presentationId,
8038
+ requestBody: {
8039
+ requests: [{ deleteObject: { objectId: a.objectId } }]
8040
+ }
8041
+ });
8042
+ return {
8043
+ content: [{ type: "text", text: `Deleted element ${a.objectId}` }],
8044
+ isError: false
8045
+ };
8046
+ }
8047
+ case "insertSlidesImageFromUrl": {
8048
+ const validation = InsertSlidesImageFromUrlSchema.safeParse(args);
8049
+ if (!validation.success) return errorResponse(validation.error.errors[0].message);
8050
+ const a = validation.data;
8051
+ return insertImageIntoSlide(ctx, a.presentationId, a.pageObjectId, a.imageUrl, a.x, a.y, a.width, a.height);
8052
+ }
8053
+ case "insertSlidesLocalImage": {
8054
+ const validation = InsertSlidesLocalImageSchema.safeParse(args);
8055
+ if (!validation.success) return errorResponse(validation.error.errors[0].message);
8056
+ const a = validation.data;
8057
+ const { fileId, webContentLink } = await uploadImageToDrive(ctx, a.localImagePath, {
8058
+ makePublic: true
8059
+ });
8060
+ try {
8061
+ const result = await insertImageIntoSlide(
8062
+ ctx,
8063
+ a.presentationId,
8064
+ a.pageObjectId,
8065
+ webContentLink,
8066
+ a.x,
8067
+ a.y,
8068
+ a.width,
8069
+ a.height
8070
+ );
8071
+ await deleteDriveFile(ctx, fileId).catch(
8072
+ (err) => ctx.log(`insertSlidesLocalImage: failed to delete intermediary Drive file ${fileId}`, err)
8073
+ );
8074
+ return result;
8075
+ } catch (err) {
8076
+ await deleteDriveFile(ctx, fileId).catch(() => {
8077
+ });
8078
+ throw err;
8079
+ }
8080
+ }
7782
8081
  default:
7783
8082
  return null;
7784
8083
  }