@piotr-agier/google-drive-mcp 1.1.2 → 1.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/README.md CHANGED
@@ -7,6 +7,7 @@ A Model Context Protocol (MCP) server that provides secure integration with Goog
7
7
  - **Multi-format Support**: Work with Google Docs, Sheets, Slides, and regular files
8
8
  - **File Management**: Create, update, delete, rename, and move files and folders
9
9
  - **Advanced Search**: Search across your entire Google Drive
10
+ - **Shared Drives Support**: Full access to Google Shared Drives (formerly Team Drives) in addition to My Drive
10
11
  - **Folder Navigation**: List and navigate through folder hierarchies with path support (e.g., `/Work/Projects`)
11
12
  - **MCP Resource Protocol**: Files accessible as MCP resources for reading content
12
13
  - **Secure Authentication**: OAuth 2.0 with automatic token refresh
@@ -496,6 +497,18 @@ Add the server to your Claude Desktop configuration:
496
497
  - `x`, `y`, `width`, `height`: Position/size in EMU
497
498
  - `backgroundColor`: Fill color (RGBA 0-1) (optional)
498
499
 
500
+ ### Speaker Notes
501
+
502
+ - **getGoogleSlidesSpeakerNotes** - Get speaker notes from a slide
503
+ - `presentationId`: Presentation ID
504
+ - `slideIndex`: Slide index (0-based)
505
+ - Returns the speaker notes text or a message if no notes exist
506
+
507
+ - **updateGoogleSlidesSpeakerNotes** - Update or set speaker notes for a slide
508
+ - `presentationId`: Presentation ID
509
+ - `slideIndex`: Slide index (0-based)
510
+ - `notes`: The speaker notes content to set
511
+
499
512
  ## Authentication Flow
500
513
 
501
514
  The server uses OAuth 2.0 for secure authentication:
package/dist/index.js CHANGED
@@ -578,7 +578,7 @@ async function authenticate() {
578
578
  // src/index.ts
579
579
  import { z } from "zod";
580
580
  import { fileURLToPath as fileURLToPath2 } from "url";
581
- import { readFileSync } from "fs";
581
+ import { readFileSync, createReadStream, existsSync, statSync } from "fs";
582
582
  import { join as join2, dirname as dirname3 } from "path";
583
583
  var drive = null;
584
584
  function ensureDriveService() {
@@ -619,6 +619,45 @@ var TEXT_MIME_TYPES = {
619
619
  txt: "text/plain",
620
620
  md: "text/markdown"
621
621
  };
622
+ var BINARY_MIME_TYPES = {
623
+ jpg: "image/jpeg",
624
+ jpeg: "image/jpeg",
625
+ png: "image/png",
626
+ gif: "image/gif",
627
+ webp: "image/webp",
628
+ svg: "image/svg+xml",
629
+ bmp: "image/bmp",
630
+ ico: "image/x-icon",
631
+ mp3: "audio/mpeg",
632
+ wav: "audio/wav",
633
+ ogg: "audio/ogg",
634
+ m4a: "audio/mp4",
635
+ aac: "audio/aac",
636
+ flac: "audio/flac",
637
+ opus: "audio/opus",
638
+ mp4: "video/mp4",
639
+ webm: "video/webm",
640
+ avi: "video/x-msvideo",
641
+ mov: "video/quicktime",
642
+ mkv: "video/x-matroska",
643
+ "3gp": "video/3gpp",
644
+ pdf: "application/pdf",
645
+ zip: "application/zip",
646
+ gz: "application/gzip",
647
+ tar: "application/x-tar",
648
+ json: "application/json",
649
+ xml: "application/xml",
650
+ csv: "text/csv",
651
+ html: "text/html",
652
+ css: "text/css",
653
+ js: "application/javascript",
654
+ doc: "application/msword",
655
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
656
+ xls: "application/vnd.ms-excel",
657
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
658
+ ppt: "application/vnd.ms-powerpoint",
659
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
660
+ };
622
661
  var authClient = null;
623
662
  var authenticationPromise = null;
624
663
  var __filename = fileURLToPath2(import.meta.url);
@@ -647,7 +686,9 @@ async function resolvePath(pathStr) {
647
686
  let response = await drive.files.list({
648
687
  q: `'${currentFolderId}' in parents and name = '${part}' and mimeType = '${FOLDER_MIME_TYPE}' and trashed = false`,
649
688
  fields: "files(id)",
650
- spaces: "drive"
689
+ spaces: "drive",
690
+ includeItemsFromAllDrives: true,
691
+ supportsAllDrives: true
651
692
  });
652
693
  if (!response.data.files?.length) {
653
694
  const folderMetadata = {
@@ -657,7 +698,8 @@ async function resolvePath(pathStr) {
657
698
  };
658
699
  const folder = await drive.files.create({
659
700
  requestBody: folderMetadata,
660
- fields: "id"
701
+ fields: "id",
702
+ supportsAllDrives: true
661
703
  });
662
704
  if (!folder.data.id) {
663
705
  throw new Error(`Failed to create intermediate folder: ${part}`);
@@ -719,7 +761,9 @@ async function checkFileExists(name, parentFolderId = "root") {
719
761
  const res = await drive.files.list({
720
762
  q: query,
721
763
  fields: "files(id, name, mimeType)",
722
- pageSize: 1
764
+ pageSize: 1,
765
+ includeItemsFromAllDrives: true,
766
+ supportsAllDrives: true
723
767
  });
724
768
  if (res.data.files && res.data.files.length > 0) {
725
769
  return res.data.files[0].id || null;
@@ -777,12 +821,14 @@ var UpdateGoogleDocSchema = z.object({
777
821
  var CreateGoogleSheetSchema = z.object({
778
822
  name: z.string().min(1, "Sheet name is required"),
779
823
  data: z.array(z.array(z.string())),
780
- parentFolderId: z.string().optional()
824
+ parentFolderId: z.string().optional(),
825
+ valueInputOption: z.enum(["RAW", "USER_ENTERED"]).optional()
781
826
  });
782
827
  var UpdateGoogleSheetSchema = z.object({
783
828
  spreadsheetId: z.string().min(1, "Spreadsheet ID is required"),
784
829
  range: z.string().min(1, "Range is required"),
785
- data: z.array(z.array(z.string()))
830
+ data: z.array(z.array(z.string())),
831
+ valueInputOption: z.enum(["RAW", "USER_ENTERED"]).optional()
786
832
  });
787
833
  var GetGoogleSheetContentSchema = z.object({
788
834
  spreadsheetId: z.string().min(1, "Spreadsheet ID is required"),
@@ -991,6 +1037,21 @@ var CreateGoogleSlidesShapeSchema = z.object({
991
1037
  alpha: z.number().min(0).max(1).optional()
992
1038
  }).optional()
993
1039
  });
1040
+ var GetGoogleSlidesSpeakerNotesSchema = z.object({
1041
+ presentationId: z.string().min(1, "Presentation ID is required"),
1042
+ slideIndex: z.number().min(0, "Slide index must be non-negative")
1043
+ });
1044
+ var UpdateGoogleSlidesSpeakerNotesSchema = z.object({
1045
+ presentationId: z.string().min(1, "Presentation ID is required"),
1046
+ slideIndex: z.number().min(0, "Slide index must be non-negative"),
1047
+ notes: z.string()
1048
+ });
1049
+ var UploadFileSchema = z.object({
1050
+ localPath: z.string().min(1, "Local file path is required"),
1051
+ name: z.string().optional(),
1052
+ parentFolderId: z.string().optional(),
1053
+ mimeType: z.string().optional()
1054
+ });
994
1055
  var server = new Server(
995
1056
  {
996
1057
  name: "google-drive-mcp",
@@ -1033,7 +1094,9 @@ server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
1033
1094
  const params = {
1034
1095
  pageSize,
1035
1096
  fields: "nextPageToken, files(id, name, mimeType)",
1036
- q: `trashed = false`
1097
+ q: `trashed = false`,
1098
+ includeItemsFromAllDrives: true,
1099
+ supportsAllDrives: true
1037
1100
  };
1038
1101
  if (request.params?.cursor) {
1039
1102
  params.pageToken = request.params.cursor;
@@ -1056,7 +1119,8 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1056
1119
  const fileId = request.params.uri.replace("gdrive:///", "");
1057
1120
  const file = await drive.files.get({
1058
1121
  fileId,
1059
- fields: "mimeType"
1122
+ fields: "mimeType",
1123
+ supportsAllDrives: true
1060
1124
  });
1061
1125
  const mimeType = file.data.mimeType;
1062
1126
  if (!mimeType) {
@@ -1082,7 +1146,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1082
1146
  break;
1083
1147
  }
1084
1148
  const res = await drive.files.export(
1085
- { fileId, mimeType: exportMimeType },
1149
+ { fileId, mimeType: exportMimeType, supportsAllDrives: true },
1086
1150
  { responseType: "text" }
1087
1151
  );
1088
1152
  log("Successfully read resource", { fileId, mimeType });
@@ -1097,7 +1161,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1097
1161
  };
1098
1162
  } else {
1099
1163
  const res = await drive.files.get(
1100
- { fileId, alt: "media" },
1164
+ { fileId, alt: "media", supportsAllDrives: true },
1101
1165
  { responseType: "arraybuffer" }
1102
1166
  );
1103
1167
  const contentMime = mimeType || "application/octet-stream";
@@ -1252,7 +1316,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1252
1316
  },
1253
1317
  {
1254
1318
  name: "createGoogleSheet",
1255
- description: "Create a new Google Sheet",
1319
+ description: "Create a new Google Sheet. By default uses RAW mode which stores values as-is. Set valueInputOption to 'USER_ENTERED' only when you need formulas to be evaluated.",
1256
1320
  inputSchema: {
1257
1321
  type: "object",
1258
1322
  properties: {
@@ -1262,22 +1326,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1262
1326
  description: "Data as array of arrays",
1263
1327
  items: { type: "array", items: { type: "string" } }
1264
1328
  },
1265
- parentFolderId: { type: "string", description: "Parent folder ID (defaults to root)", optional: true }
1329
+ parentFolderId: { type: "string", description: "Parent folder ID (defaults to root)", optional: true },
1330
+ valueInputOption: {
1331
+ type: "string",
1332
+ enum: ["RAW", "USER_ENTERED"],
1333
+ description: "RAW (default): Values stored exactly as provided - formulas stored as text strings. Safe for untrusted data. USER_ENTERED: Values parsed like spreadsheet UI - formulas (=SUM, =IF, etc.) are evaluated. SECURITY WARNING: USER_ENTERED can execute formulas, only use with trusted data, never with user-provided input that could contain malicious formulas like =IMPORTDATA() or =IMPORTRANGE()."
1334
+ }
1266
1335
  },
1267
1336
  required: ["name", "data"]
1268
1337
  }
1269
1338
  },
1270
1339
  {
1271
1340
  name: "updateGoogleSheet",
1272
- description: "Update an existing Google Sheet",
1341
+ description: "Update an existing Google Sheet. By default uses RAW mode which stores values as-is. Set valueInputOption to 'USER_ENTERED' only when you need formulas to be evaluated.",
1273
1342
  inputSchema: {
1274
1343
  type: "object",
1275
1344
  properties: {
1276
1345
  spreadsheetId: { type: "string", description: "Sheet ID" },
1277
- range: { type: "string", description: "Range to update" },
1346
+ range: { type: "string", description: "Range to update (e.g., 'Sheet1!A1:C10')" },
1278
1347
  data: {
1279
1348
  type: "array",
1349
+ description: "2D array of values to write",
1280
1350
  items: { type: "array", items: { type: "string" } }
1351
+ },
1352
+ valueInputOption: {
1353
+ type: "string",
1354
+ enum: ["RAW", "USER_ENTERED"],
1355
+ description: "RAW (default): Values stored exactly as provided - formulas stored as text strings. Safe for untrusted data. USER_ENTERED: Values parsed like spreadsheet UI - formulas (=SUM, =IF, etc.) are evaluated. SECURITY WARNING: USER_ENTERED can execute formulas, only use with trusted data, never with user-provided input that could contain malicious formulas like =IMPORTDATA() or =IMPORTRANGE()."
1281
1356
  }
1282
1357
  },
1283
1358
  required: ["spreadsheetId", "range", "data"]
@@ -1787,6 +1862,45 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1787
1862
  },
1788
1863
  required: ["presentationId", "pageObjectId", "shapeType", "x", "y", "width", "height"]
1789
1864
  }
1865
+ },
1866
+ {
1867
+ name: "getGoogleSlidesSpeakerNotes",
1868
+ description: "Get speaker notes from a specific slide in Google Slides",
1869
+ inputSchema: {
1870
+ type: "object",
1871
+ properties: {
1872
+ presentationId: { type: "string", description: "Presentation ID" },
1873
+ slideIndex: { type: "number", description: "Slide index (0-based)" }
1874
+ },
1875
+ required: ["presentationId", "slideIndex"]
1876
+ }
1877
+ },
1878
+ {
1879
+ name: "updateGoogleSlidesSpeakerNotes",
1880
+ description: "Update speaker notes for a specific slide in Google Slides",
1881
+ inputSchema: {
1882
+ type: "object",
1883
+ properties: {
1884
+ presentationId: { type: "string", description: "Presentation ID" },
1885
+ slideIndex: { type: "number", description: "Slide index (0-based)" },
1886
+ notes: { type: "string", description: "Speaker notes content" }
1887
+ },
1888
+ required: ["presentationId", "slideIndex", "notes"]
1889
+ }
1890
+ },
1891
+ {
1892
+ name: "uploadFile",
1893
+ description: "Upload a local file (any type: image, audio, video, PDF, etc.) to Google Drive",
1894
+ inputSchema: {
1895
+ type: "object",
1896
+ properties: {
1897
+ localPath: { type: "string", description: "Absolute path to the local file to upload" },
1898
+ name: { type: "string", description: "File name in Drive (defaults to local filename)", optional: true },
1899
+ parentFolderId: { type: "string", description: "Parent folder ID or path (e.g., '/Work/Projects'). Creates folders if needed. Defaults to root.", optional: true },
1900
+ mimeType: { type: "string", description: "MIME type (auto-detected from extension if omitted)", optional: true }
1901
+ },
1902
+ required: ["localPath"]
1903
+ }
1790
1904
  }
1791
1905
  ]
1792
1906
  };
@@ -1814,9 +1928,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1814
1928
  q: formattedQuery,
1815
1929
  pageSize: Math.min(pageSize || 50, 100),
1816
1930
  pageToken,
1817
- fields: "nextPageToken, files(id, name, mimeType, modifiedTime, size)"
1931
+ fields: "nextPageToken, files(id, name, mimeType, modifiedTime, size)",
1932
+ includeItemsFromAllDrives: true,
1933
+ supportsAllDrives: true
1818
1934
  });
1819
- const fileList = res.data.files?.map((f) => `${f.name} (${f.mimeType})`).join("\n") || "";
1935
+ const fileList = res.data.files?.map((f) => `${f.name} (ID: ${f.id}, ${f.mimeType})`).join("\n") || "";
1820
1936
  log("Search results", { query: userQuery, resultCount: res.data.files?.length });
1821
1937
  let response = `Found ${res.data.files?.length ?? 0} files:
1822
1938
  ${fileList}`;
@@ -1860,7 +1976,8 @@ More results available. Use pageToken: ${res.data.nextPageToken}`;
1860
1976
  media: {
1861
1977
  mimeType: fileMetadata.mimeType,
1862
1978
  body: args.content
1863
- }
1979
+ },
1980
+ supportsAllDrives: true
1864
1981
  });
1865
1982
  log("File created successfully", { fileId: file.data?.id });
1866
1983
  return {
@@ -1880,7 +1997,8 @@ ID: ${file.data?.id || "unknown"}`
1880
1997
  const args = validation.data;
1881
1998
  const existingFile = await drive.files.get({
1882
1999
  fileId: args.fileId,
1883
- fields: "mimeType, name, parents"
2000
+ fields: "mimeType, name, parents",
2001
+ supportsAllDrives: true
1884
2002
  });
1885
2003
  const currentMimeType = existingFile.data.mimeType || "text/plain";
1886
2004
  if (!Object.values(TEXT_MIME_TYPES).includes(currentMimeType)) {
@@ -1899,7 +2017,8 @@ ID: ${file.data?.id || "unknown"}`
1899
2017
  mimeType: updateMetadata.mimeType || currentMimeType,
1900
2018
  body: args.content
1901
2019
  },
1902
- fields: "id, name, modifiedTime, webViewLink"
2020
+ fields: "id, name, modifiedTime, webViewLink",
2021
+ supportsAllDrives: true
1903
2022
  });
1904
2023
  return {
1905
2024
  content: [{
@@ -1930,7 +2049,8 @@ Modified: ${updatedFile.data.modifiedTime}`
1930
2049
  };
1931
2050
  const folder = await drive.files.create({
1932
2051
  requestBody: folderMetadata,
1933
- fields: "id, name, webViewLink"
2052
+ fields: "id, name, webViewLink",
2053
+ supportsAllDrives: true
1934
2054
  });
1935
2055
  log("Folder created successfully", { folderId: folder.data.id, name: folder.data.name });
1936
2056
  return {
@@ -1954,7 +2074,9 @@ ID: ${folder.data.id}`
1954
2074
  pageSize: Math.min(args.pageSize || 50, 100),
1955
2075
  pageToken: args.pageToken,
1956
2076
  fields: "nextPageToken, files(id, name, mimeType, modifiedTime, size)",
1957
- orderBy: "name"
2077
+ orderBy: "name",
2078
+ includeItemsFromAllDrives: true,
2079
+ supportsAllDrives: true
1958
2080
  });
1959
2081
  const files = res.data.files || [];
1960
2082
  const formattedFiles = files.map((file) => {
@@ -1980,12 +2102,13 @@ More items available. Use pageToken: ${res.data.nextPageToken}`;
1980
2102
  return errorResponse(validation.error.errors[0].message);
1981
2103
  }
1982
2104
  const args = validation.data;
1983
- const item = await drive.files.get({ fileId: args.itemId, fields: "name" });
2105
+ const item = await drive.files.get({ fileId: args.itemId, fields: "name", supportsAllDrives: true });
1984
2106
  await drive.files.update({
1985
2107
  fileId: args.itemId,
1986
2108
  requestBody: {
1987
2109
  trashed: true
1988
- }
2110
+ },
2111
+ supportsAllDrives: true
1989
2112
  });
1990
2113
  log("Item moved to trash successfully", { itemId: args.itemId, name: item.data.name });
1991
2114
  return {
@@ -1999,14 +2122,15 @@ More items available. Use pageToken: ${res.data.nextPageToken}`;
1999
2122
  return errorResponse(validation.error.errors[0].message);
2000
2123
  }
2001
2124
  const args = validation.data;
2002
- const item = await drive.files.get({ fileId: args.itemId, fields: "name, mimeType" });
2125
+ const item = await drive.files.get({ fileId: args.itemId, fields: "name, mimeType", supportsAllDrives: true });
2003
2126
  if (Object.values(TEXT_MIME_TYPES).includes(item.data.mimeType || "")) {
2004
2127
  validateTextFileExtension(args.newName);
2005
2128
  }
2006
2129
  const updatedItem = await drive.files.update({
2007
2130
  fileId: args.itemId,
2008
2131
  requestBody: { name: args.newName },
2009
- fields: "id, name, modifiedTime"
2132
+ fields: "id, name, modifiedTime",
2133
+ supportsAllDrives: true
2010
2134
  });
2011
2135
  return {
2012
2136
  content: [{
@@ -2026,16 +2150,18 @@ More items available. Use pageToken: ${res.data.nextPageToken}`;
2026
2150
  if (args.destinationFolderId === args.itemId) {
2027
2151
  return errorResponse("Cannot move a folder into itself.");
2028
2152
  }
2029
- const item = await drive.files.get({ fileId: args.itemId, fields: "name, parents" });
2153
+ const item = await drive.files.get({ fileId: args.itemId, fields: "name, parents", supportsAllDrives: true });
2030
2154
  await drive.files.update({
2031
2155
  fileId: args.itemId,
2032
2156
  addParents: destinationFolderId,
2033
2157
  removeParents: item.data.parents?.join(",") || "",
2034
- fields: "id, name, parents"
2158
+ fields: "id, name, parents",
2159
+ supportsAllDrives: true
2035
2160
  });
2036
2161
  const destinationFolder = await drive.files.get({
2037
2162
  fileId: destinationFolderId,
2038
- fields: "name"
2163
+ fields: "name",
2164
+ supportsAllDrives: true
2039
2165
  });
2040
2166
  return {
2041
2167
  content: [{
@@ -2079,7 +2205,8 @@ More items available. Use pageToken: ${res.data.nextPageToken}`;
2079
2205
  mimeType: "application/vnd.google-apps.document",
2080
2206
  parents: [parentFolderId]
2081
2207
  },
2082
- fields: "id, name, webViewLink"
2208
+ fields: "id, name, webViewLink",
2209
+ supportsAllDrives: true
2083
2210
  });
2084
2211
  } catch (createError) {
2085
2212
  log("Drive files.create error details:", {
@@ -2204,12 +2331,14 @@ Link: ${doc.webViewLink}` }],
2204
2331
  await drive.files.update({
2205
2332
  fileId: spreadsheet.data.spreadsheetId || "",
2206
2333
  addParents: parentFolderId,
2207
- fields: "id, name, webViewLink"
2334
+ removeParents: "root",
2335
+ fields: "id, name, webViewLink",
2336
+ supportsAllDrives: true
2208
2337
  });
2209
2338
  await sheets.spreadsheets.values.update({
2210
2339
  spreadsheetId: spreadsheet.data.spreadsheetId,
2211
2340
  range: "Sheet1!A1",
2212
- valueInputOption: "RAW",
2341
+ valueInputOption: args.valueInputOption || "RAW",
2213
2342
  requestBody: { values: args.data }
2214
2343
  });
2215
2344
  return {
@@ -2228,7 +2357,7 @@ ID: ${spreadsheet.data.spreadsheetId}` }],
2228
2357
  await sheets.spreadsheets.values.update({
2229
2358
  spreadsheetId: args.spreadsheetId,
2230
2359
  range: args.range,
2231
- valueInputOption: "RAW",
2360
+ valueInputOption: args.valueInputOption || "RAW",
2232
2361
  requestBody: { values: args.data }
2233
2362
  });
2234
2363
  return {
@@ -2626,7 +2755,8 @@ ID: ${spreadsheet.data.spreadsheetId}` }],
2626
2755
  await drive.files.update({
2627
2756
  fileId: presentation.data.presentationId,
2628
2757
  addParents: parentFolderId,
2629
- removeParents: "root"
2758
+ removeParents: "root",
2759
+ supportsAllDrives: true
2630
2760
  });
2631
2761
  for (const slide of args.slides) {
2632
2762
  const slideObjectId = `slide_${uuidv4().substring(0, 8)}`;
@@ -3421,6 +3551,148 @@ Slide ${args.slideIndex ?? index} (ID: ${slide.objectId}):
3421
3551
  isError: false
3422
3552
  };
3423
3553
  }
3554
+ case "getGoogleSlidesSpeakerNotes": {
3555
+ const validation = GetGoogleSlidesSpeakerNotesSchema.safeParse(request.params.arguments);
3556
+ if (!validation.success) {
3557
+ return errorResponse(validation.error.errors[0].message);
3558
+ }
3559
+ const args = validation.data;
3560
+ const slidesService = google.slides({ version: "v1", auth: authClient });
3561
+ const presentation = await slidesService.presentations.get({
3562
+ presentationId: args.presentationId
3563
+ });
3564
+ if (!presentation.data.slides || args.slideIndex >= presentation.data.slides.length) {
3565
+ return errorResponse(`Slide index ${args.slideIndex} not found in presentation (has ${presentation.data.slides?.length ?? 0} slides)`);
3566
+ }
3567
+ const slide = presentation.data.slides[args.slideIndex];
3568
+ const notesObjectId = slide.slideProperties?.notesPage?.notesProperties?.speakerNotesObjectId;
3569
+ if (!notesObjectId) {
3570
+ return {
3571
+ content: [{ type: "text", text: "No speaker notes found for this slide" }],
3572
+ isError: false
3573
+ };
3574
+ }
3575
+ const notesPageObjectId = slide.slideProperties?.notesPage?.objectId;
3576
+ if (!notesPageObjectId) {
3577
+ return {
3578
+ content: [{ type: "text", text: "No speaker notes found for this slide" }],
3579
+ isError: false
3580
+ };
3581
+ }
3582
+ const notesPage = presentation.data.slides?.[args.slideIndex]?.slideProperties?.notesPage;
3583
+ if (!notesPage || !notesPage.pageElements) {
3584
+ return {
3585
+ content: [{ type: "text", text: "No speaker notes found for this slide" }],
3586
+ isError: false
3587
+ };
3588
+ }
3589
+ const speakerNotesElement = notesPage.pageElements.find(
3590
+ (element) => element.objectId === notesObjectId
3591
+ );
3592
+ if (!speakerNotesElement || !speakerNotesElement.shape?.text) {
3593
+ return {
3594
+ content: [{ type: "text", text: "No speaker notes found for this slide" }],
3595
+ isError: false
3596
+ };
3597
+ }
3598
+ let notesText = "";
3599
+ const textElements = speakerNotesElement.shape.text.textElements || [];
3600
+ textElements.forEach((textElement) => {
3601
+ if (textElement.textRun?.content) {
3602
+ notesText += textElement.textRun.content;
3603
+ }
3604
+ });
3605
+ return {
3606
+ content: [{ type: "text", text: notesText.trim() || "No speaker notes found for this slide" }],
3607
+ isError: false
3608
+ };
3609
+ }
3610
+ case "updateGoogleSlidesSpeakerNotes": {
3611
+ const validation = UpdateGoogleSlidesSpeakerNotesSchema.safeParse(request.params.arguments);
3612
+ if (!validation.success) {
3613
+ return errorResponse(validation.error.errors[0].message);
3614
+ }
3615
+ const args = validation.data;
3616
+ const slidesService = google.slides({ version: "v1", auth: authClient });
3617
+ const presentation = await slidesService.presentations.get({
3618
+ presentationId: args.presentationId
3619
+ });
3620
+ if (!presentation.data.slides || args.slideIndex >= presentation.data.slides.length) {
3621
+ return errorResponse(`Slide index ${args.slideIndex} not found in presentation (has ${presentation.data.slides?.length ?? 0} slides)`);
3622
+ }
3623
+ const slide = presentation.data.slides[args.slideIndex];
3624
+ const notesObjectId = slide.slideProperties?.notesPage?.notesProperties?.speakerNotesObjectId;
3625
+ if (!notesObjectId) {
3626
+ return errorResponse("This slide does not have a speaker notes object. Speaker notes may need to be initialized manually in Google Slides first.");
3627
+ }
3628
+ const requests = [
3629
+ {
3630
+ deleteText: {
3631
+ objectId: notesObjectId,
3632
+ textRange: {
3633
+ type: "ALL"
3634
+ }
3635
+ }
3636
+ },
3637
+ {
3638
+ insertText: {
3639
+ objectId: notesObjectId,
3640
+ text: args.notes,
3641
+ insertionIndex: 0
3642
+ }
3643
+ }
3644
+ ];
3645
+ await slidesService.presentations.batchUpdate({
3646
+ presentationId: args.presentationId,
3647
+ requestBody: { requests }
3648
+ });
3649
+ return {
3650
+ content: [{ type: "text", text: `Successfully updated speaker notes for slide ${args.slideIndex}` }],
3651
+ isError: false
3652
+ };
3653
+ }
3654
+ case "uploadFile": {
3655
+ const validation = UploadFileSchema.safeParse(request.params.arguments);
3656
+ if (!validation.success) {
3657
+ return errorResponse(validation.error.errors[0].message);
3658
+ }
3659
+ const args = validation.data;
3660
+ if (!existsSync(args.localPath)) {
3661
+ return errorResponse(`File not found: ${args.localPath}`);
3662
+ }
3663
+ const stats = statSync(args.localPath);
3664
+ const fileName = args.name || args.localPath.split(/[\\/]/).pop() || "upload";
3665
+ const ext = fileName.split(".").pop()?.toLowerCase() || "";
3666
+ const detectedMime = args.mimeType || BINARY_MIME_TYPES[ext] || "application/octet-stream";
3667
+ const parentId = await resolveFolderId(args.parentFolderId);
3668
+ log("Uploading file", { localPath: args.localPath, name: fileName, mimeType: detectedMime, size: stats.size });
3669
+ const file = await drive.files.create({
3670
+ requestBody: {
3671
+ name: fileName,
3672
+ parents: [parentId]
3673
+ },
3674
+ media: {
3675
+ mimeType: detectedMime,
3676
+ body: createReadStream(args.localPath)
3677
+ },
3678
+ fields: "id, name, size, mimeType, webViewLink",
3679
+ supportsAllDrives: true
3680
+ });
3681
+ log("File uploaded successfully", { fileId: file.data?.id });
3682
+ return {
3683
+ content: [{
3684
+ type: "text",
3685
+ text: [
3686
+ `Uploaded: ${file.data?.name || fileName}`,
3687
+ `ID: ${file.data?.id || "unknown"}`,
3688
+ `Size: ${file.data?.size || stats.size} bytes`,
3689
+ `Type: ${file.data?.mimeType || detectedMime}`,
3690
+ file.data?.webViewLink ? `Link: ${file.data.webViewLink}` : ""
3691
+ ].filter(Boolean).join("\n")
3692
+ }],
3693
+ isError: false
3694
+ };
3695
+ }
3424
3696
  default:
3425
3697
  return errorResponse("Tool not found");
3426
3698
  }