@piotr-agier/google-drive-mcp 1.7.5 → 1.7.6

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
@@ -189,21 +189,14 @@ npx @piotr-agier/google-drive-mcp auth
189
189
 
190
190
  ### Running the Docker Container
191
191
 
192
- Run the container with your credentials and tokens mounted:
192
+ The `scripts/docker-mcp.sh` wrapper manages the container lifecycle it creates, reuses, and replaces containers automatically. MCP clients invoke this script directly (see configuration below).
193
+
194
+ To verify the image works after a rebuild:
193
195
 
194
196
  ```bash
195
- docker run -it \
196
- -v /path/to/gcp-oauth.keys.json:/config/gcp-oauth.keys.json:ro \
197
- -v "$HOME/.config/google-drive-mcp/tokens.json":/config/tokens.json \
198
- google-drive-mcp
197
+ docker run --rm google-drive-mcp --help
199
198
  ```
200
199
 
201
- **Important Notes:**
202
- - Replace `/path/to/gcp-oauth.keys.json` with the actual path to your OAuth credentials
203
- - The `:ro` flag mounts the credentials as read-only for security
204
- - Tokens are mounted read-write to allow automatic refresh
205
- - The container runs as non-root user for security
206
-
207
200
  ### Docker Configuration for Claude Desktop
208
201
 
209
202
  #### Option A: Reusable container (recommended)
@@ -228,6 +221,7 @@ The script will:
228
221
  - Create the container on first run
229
222
  - Reuse the existing container on subsequent runs
230
223
  - Automatically restart it if it was stopped
224
+ - Replace the container when the image has been rebuilt
231
225
 
232
226
  **Note:** The container stays running in the background until explicitly stopped.
233
227
  To stop it: `docker stop google-drive-mcp`
@@ -525,6 +519,12 @@ Add the server to your Claude Desktop configuration:
525
519
  - **readSmartChips** - Read smart chip-like elements (person mentions, rich links, date chips) from the default tab of a document. Only the default tab is scanned; other tabs are not included.
526
520
  - `documentId`: Document ID
527
521
 
522
+ - **createFootnote** - Create a footnote in a Google Doc. Footnotes cannot be inserted inside equations, headers, footers, or other footnotes.
523
+ - `documentId`: Document ID
524
+ - `index`: 1-based character index where the footnote reference should be inserted (optional — provide this or `endOfSegment`)
525
+ - `endOfSegment`: If true, insert footnote at the end of the document body (optional — provide this or `index`)
526
+ - `content`: Optional text content for the footnote body
527
+
528
528
  - **listGoogleDocs** - List Google Documents with optional filtering
529
529
  - `query`: Search query to filter by name or content (optional)
530
530
  - `maxResults`: Maximum documents to return, 1-100 (optional, default: 20)
@@ -1081,11 +1081,9 @@ npx @piotr-agier/google-drive-mcp auth
1081
1081
  # 2. Verify tokens exist
1082
1082
  ls -la ~/.config/google-drive-mcp/tokens.json
1083
1083
 
1084
- # 3. Run Docker with tokens mounted
1085
- docker run -it \
1086
- -v $(pwd)/gcp-oauth.keys.json:/config/gcp-oauth.keys.json:ro \
1087
- -v "$HOME/.config/google-drive-mcp/tokens.json":/config/tokens.json \
1088
- google-drive-mcp
1084
+ # 3. Rebuild the image and restart the client
1085
+ docker build -t google-drive-mcp .
1086
+ # The client will invoke scripts/docker-mcp.sh, which auto-replaces the stale container
1089
1087
  ```
1090
1088
 
1091
1089
  #### "npm ci failed" during Docker build
package/dist/index.js CHANGED
@@ -892,6 +892,7 @@ async function downloadDriveFile(drive, args, log2) {
892
892
 
893
893
  // src/tools/drive.ts
894
894
  var FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";
895
+ var SHORTCUT_MIME_TYPE = "application/vnd.google-apps.shortcut";
895
896
  var BINARY_MIME_TYPES = {
896
897
  jpg: "image/jpeg",
897
898
  jpeg: "image/jpeg",
@@ -976,6 +977,19 @@ var CopyFileSchema = z.object({
976
977
  newName: z.string().optional(),
977
978
  parentFolderId: z.string().optional()
978
979
  });
980
+ var CreateShortcutSchema = z.object({
981
+ targetFileId: z.string().min(1, "Target file ID is required"),
982
+ parentFolderId: z.string().optional(),
983
+ shortcutName: z.string().optional()
984
+ });
985
+ var LockFileSchema = z.object({
986
+ fileId: z.string().min(1, "File ID is required"),
987
+ reason: z.string().optional(),
988
+ ownerRestricted: z.boolean().optional()
989
+ });
990
+ var UnlockFileSchema = z.object({
991
+ fileId: z.string().min(1, "File ID is required")
992
+ });
979
993
  var UploadFileSchema = z.object({
980
994
  localPath: z.string().min(1, "Local file path is required"),
981
995
  name: z.string().optional(),
@@ -1200,7 +1214,7 @@ var toolDefinitions = [
1200
1214
  properties: {
1201
1215
  fileId: { type: "string", description: "ID of the file to copy" },
1202
1216
  newName: { type: "string", description: "Name for the copied file. If not provided, will use 'Copy of [original name]'" },
1203
- parentFolderId: { type: "string", description: "ID of folder where copy should be placed. If not provided, places in same location as original." }
1217
+ parentFolderId: { type: "string", description: "ID or path of the destination folder (defaults to same folder as original)" }
1204
1218
  },
1205
1219
  required: ["fileId"]
1206
1220
  }
@@ -1392,6 +1406,64 @@ var toolDefinitions = [
1392
1406
  fileId: { type: "string", description: "Optional file ID for targeted access check" }
1393
1407
  }
1394
1408
  }
1409
+ },
1410
+ {
1411
+ name: "createShortcut",
1412
+ description: "Create a shortcut (link) to a file or folder in Google Drive. Useful for referencing the same document from multiple locations without duplicating it.",
1413
+ inputSchema: {
1414
+ type: "object",
1415
+ properties: {
1416
+ targetFileId: {
1417
+ type: "string",
1418
+ description: "The file or folder ID (not a path) to create a shortcut to"
1419
+ },
1420
+ parentFolderId: {
1421
+ type: "string",
1422
+ description: "ID or path of the folder where the shortcut will be created"
1423
+ },
1424
+ shortcutName: {
1425
+ type: "string",
1426
+ description: "Custom name for the shortcut (defaults to original file name)"
1427
+ }
1428
+ },
1429
+ required: ["targetFileId"]
1430
+ }
1431
+ },
1432
+ {
1433
+ name: "lockFile",
1434
+ description: "Lock a file to prevent editing by setting content restrictions. The file remains readable but cannot be modified until unlocked.",
1435
+ inputSchema: {
1436
+ type: "object",
1437
+ properties: {
1438
+ fileId: {
1439
+ type: "string",
1440
+ description: "ID of the file to lock"
1441
+ },
1442
+ reason: {
1443
+ type: "string",
1444
+ description: "Reason for locking the file (shown to users who try to edit)"
1445
+ },
1446
+ ownerRestricted: {
1447
+ type: "boolean",
1448
+ description: "If true, only the file owner can unlock the file (default: false)"
1449
+ }
1450
+ },
1451
+ required: ["fileId"]
1452
+ }
1453
+ },
1454
+ {
1455
+ name: "unlockFile",
1456
+ description: "Unlock a previously locked file by removing content restrictions, restoring full edit access.",
1457
+ inputSchema: {
1458
+ type: "object",
1459
+ properties: {
1460
+ fileId: {
1461
+ type: "string",
1462
+ description: "ID of the file to unlock"
1463
+ }
1464
+ },
1465
+ required: ["fileId"]
1466
+ }
1395
1467
  }
1396
1468
  ];
1397
1469
  async function handleTool(toolName, args, ctx) {
@@ -1729,7 +1801,8 @@ More results available. Use pageToken: ${res.data.nextPageToken}`;
1729
1801
  name: data.newName || `Copy of ${originalFile.data.name}`
1730
1802
  };
1731
1803
  if (data.parentFolderId) {
1732
- copyMetadata.parents = [data.parentFolderId];
1804
+ const resolvedParentId = await ctx.resolveFolderId(data.parentFolderId);
1805
+ copyMetadata.parents = [resolvedParentId];
1733
1806
  } else if (originalFile.data.parents) {
1734
1807
  copyMetadata.parents = originalFile.data.parents;
1735
1808
  }
@@ -1746,6 +1819,136 @@ Link: ${response.data.webViewLink}` }],
1746
1819
  isError: false
1747
1820
  };
1748
1821
  }
1822
+ case "createShortcut": {
1823
+ const validation = CreateShortcutSchema.safeParse(args);
1824
+ if (!validation.success) {
1825
+ return errorResponse(validation.error.errors[0].message);
1826
+ }
1827
+ const data = validation.data;
1828
+ const parentId = await ctx.resolveFolderId(data.parentFolderId);
1829
+ const targetFile = await ctx.getDrive().files.get({
1830
+ fileId: data.targetFileId,
1831
+ fields: "id, name, mimeType",
1832
+ supportsAllDrives: true
1833
+ });
1834
+ const shortcutName = data.shortcutName || targetFile.data.name || "Shortcut";
1835
+ const shortcut = await ctx.getDrive().files.create({
1836
+ requestBody: {
1837
+ name: shortcutName,
1838
+ mimeType: SHORTCUT_MIME_TYPE,
1839
+ shortcutDetails: {
1840
+ targetId: data.targetFileId
1841
+ },
1842
+ parents: [parentId]
1843
+ },
1844
+ fields: "id, name, webViewLink, shortcutDetails",
1845
+ supportsAllDrives: true
1846
+ });
1847
+ ctx.log("Shortcut created", {
1848
+ shortcutId: shortcut.data.id,
1849
+ targetId: data.targetFileId,
1850
+ name: shortcutName
1851
+ });
1852
+ return {
1853
+ content: [{
1854
+ type: "text",
1855
+ text: `Shortcut created successfully!
1856
+
1857
+ Shortcut: ${shortcut.data.name} (${shortcut.data.id})
1858
+ Target: ${targetFile.data.name} (${data.targetFileId})
1859
+ Location: folder ${parentId}
1860
+ Link: ${shortcut.data.webViewLink || "N/A"}`
1861
+ }],
1862
+ isError: false
1863
+ };
1864
+ }
1865
+ case "lockFile": {
1866
+ const validation = LockFileSchema.safeParse(args);
1867
+ if (!validation.success) {
1868
+ return errorResponse(validation.error.errors[0].message);
1869
+ }
1870
+ const data = validation.data;
1871
+ const fileInfo = await ctx.getDrive().files.get({
1872
+ fileId: data.fileId,
1873
+ fields: "id, name, contentRestrictions",
1874
+ supportsAllDrives: true
1875
+ });
1876
+ const existingRestrictions = fileInfo.data.contentRestrictions || [];
1877
+ if (existingRestrictions.some((r) => r.readOnly)) {
1878
+ return {
1879
+ content: [{
1880
+ type: "text",
1881
+ text: `File "${fileInfo.data.name}" is already locked.`
1882
+ }],
1883
+ isError: false
1884
+ };
1885
+ }
1886
+ await ctx.getDrive().files.update({
1887
+ fileId: data.fileId,
1888
+ requestBody: {
1889
+ contentRestrictions: [{
1890
+ readOnly: true,
1891
+ reason: data.reason || "Locked via MCP",
1892
+ ownerRestricted: data.ownerRestricted ?? false
1893
+ }]
1894
+ },
1895
+ supportsAllDrives: true
1896
+ });
1897
+ ctx.log("File locked", { fileId: data.fileId, name: fileInfo.data.name, reason: data.reason });
1898
+ return {
1899
+ content: [{
1900
+ type: "text",
1901
+ text: `File locked successfully!
1902
+
1903
+ File: ${fileInfo.data.name}
1904
+ Reason: ${data.reason || "Locked via MCP"}${data.ownerRestricted ? "\nOwner-restricted: only the file owner can unlock" : ""}
1905
+
1906
+ The file is now read-only and cannot be edited or deleted.`
1907
+ }],
1908
+ isError: false
1909
+ };
1910
+ }
1911
+ case "unlockFile": {
1912
+ const validation = UnlockFileSchema.safeParse(args);
1913
+ if (!validation.success) {
1914
+ return errorResponse(validation.error.errors[0].message);
1915
+ }
1916
+ const data = validation.data;
1917
+ const fileInfo = await ctx.getDrive().files.get({
1918
+ fileId: data.fileId,
1919
+ fields: "id, name, contentRestrictions",
1920
+ supportsAllDrives: true
1921
+ });
1922
+ const existingRestrictions = fileInfo.data.contentRestrictions || [];
1923
+ if (!existingRestrictions.some((r) => r.readOnly)) {
1924
+ return {
1925
+ content: [{
1926
+ type: "text",
1927
+ text: `File "${fileInfo.data.name}" is not locked.`
1928
+ }],
1929
+ isError: false
1930
+ };
1931
+ }
1932
+ await ctx.getDrive().files.update({
1933
+ fileId: data.fileId,
1934
+ requestBody: {
1935
+ contentRestrictions: [{ readOnly: false }]
1936
+ },
1937
+ supportsAllDrives: true
1938
+ });
1939
+ ctx.log("File unlocked", { fileId: data.fileId, name: fileInfo.data.name });
1940
+ return {
1941
+ content: [{
1942
+ type: "text",
1943
+ text: `File unlocked successfully!
1944
+
1945
+ File: ${fileInfo.data.name}
1946
+
1947
+ The file can now be edited and deleted.`
1948
+ }],
1949
+ isError: false
1950
+ };
1951
+ }
1749
1952
  case "uploadFile": {
1750
1953
  const validation = UploadFileSchema.safeParse(args);
1751
1954
  if (!validation.success) {
@@ -3046,6 +3249,14 @@ var InsertSmartChipSchema = z2.object({
3046
3249
  var ReadSmartChipsSchema = z2.object({
3047
3250
  documentId: z2.string().min(1, "Document ID is required")
3048
3251
  });
3252
+ var CreateFootnoteSchema = z2.object({
3253
+ documentId: z2.string().min(1, "Document ID is required"),
3254
+ index: z2.number().int().min(1, "Index must be at least 1").optional(),
3255
+ endOfSegment: z2.boolean().optional(),
3256
+ content: z2.string().optional()
3257
+ }).refine((data) => data.index !== void 0 || data.endOfSegment === true, {
3258
+ message: "Either 'index' or 'endOfSegment: true' must be provided"
3259
+ });
3049
3260
  var toolDefinitions2 = [
3050
3261
  {
3051
3262
  name: "createGoogleDoc",
@@ -3465,6 +3676,20 @@ var toolDefinitions2 = [
3465
3676
  },
3466
3677
  required: ["documentId"]
3467
3678
  }
3679
+ },
3680
+ {
3681
+ name: "createFootnote",
3682
+ description: "Create a footnote in a Google Doc. Footnotes cannot be inserted inside equations, headers, footers, or other footnotes.",
3683
+ inputSchema: {
3684
+ type: "object",
3685
+ properties: {
3686
+ documentId: { type: "string", description: "Document ID" },
3687
+ index: { type: "number", description: "1-based character index where the footnote reference should be inserted" },
3688
+ endOfSegment: { type: "boolean", description: "If true, insert footnote at the end of the document body (use instead of index)" },
3689
+ content: { type: "string", description: "Optional text content for the footnote body" }
3690
+ },
3691
+ required: ["documentId"]
3692
+ }
3468
3693
  }
3469
3694
  ];
3470
3695
  async function handleTool2(toolName, args, ctx) {
@@ -3590,37 +3815,106 @@ Link: ${doc.webViewLink}` }],
3590
3815
  // DOC CONTENT
3591
3816
  // =========================================================================
3592
3817
  case "getGoogleDocContent": {
3593
- let extractSegments2 = function(bodyContent) {
3818
+ let resolveInlineElementText2 = function(el, inlineObjects) {
3819
+ if (el.person?.personProperties) {
3820
+ const p = el.person.personProperties;
3821
+ if (p.name && p.email) return `@${p.name} (${p.email})`;
3822
+ return `@${p.name || p.email || ""}`;
3823
+ }
3824
+ if (el.richLink?.richLinkProperties) {
3825
+ const rl = el.richLink.richLinkProperties;
3826
+ const title = (rl.title || rl.uri || "").replace(/[\[\]]/g, "\\$&");
3827
+ const uri = rl.uri;
3828
+ return title && uri ? `[${title}](${uri})` : title || null;
3829
+ }
3830
+ if (el.inlineObjectElement?.inlineObjectId) {
3831
+ if (inlineObjects) {
3832
+ const obj = inlineObjects[el.inlineObjectElement.inlineObjectId];
3833
+ const desc = obj?.inlineObjectProperties?.embeddedObject?.description || obj?.inlineObjectProperties?.embeddedObject?.title;
3834
+ return desc ? `[image: ${desc}]` : "[image]";
3835
+ }
3836
+ return "[image]";
3837
+ }
3838
+ if (el.footnoteReference) {
3839
+ return `[^${el.footnoteReference.footnoteNumber || ""}]`;
3840
+ }
3841
+ if (el.horizontalRule) {
3842
+ return "---\n";
3843
+ }
3844
+ return null;
3845
+ }, extractSegments2 = function(bodyContent, inlineObjects) {
3594
3846
  const segments = [];
3595
- for (const element of bodyContent) {
3596
- if (element.paragraph?.elements) {
3597
- for (const textElement of element.paragraph.elements) {
3598
- if (textElement.textRun?.content && textElement.startIndex != null && textElement.endIndex != null) {
3599
- const seg = {
3600
- text: textElement.textRun.content,
3601
- startIndex: textElement.startIndex,
3602
- endIndex: textElement.endIndex
3603
- };
3604
- if (withFormatting) {
3605
- const ts = textElement.textRun.textStyle;
3606
- if (ts) {
3607
- if (ts.weightedFontFamily?.fontFamily) seg.fontFamily = ts.weightedFontFamily.fontFamily;
3608
- if (ts.fontSize?.magnitude != null) seg.fontSize = ts.fontSize.magnitude;
3609
- if (ts.bold) seg.bold = true;
3610
- if (ts.italic) seg.italic = true;
3611
- if (ts.underline) seg.underline = true;
3612
- if (ts.strikethrough) seg.strikethrough = true;
3613
- const fg = rgbColorToHex(ts.foregroundColor);
3614
- const bg = rgbColorToHex(ts.backgroundColor);
3615
- if (fg) seg.foregroundColor = fg;
3616
- if (bg) seg.backgroundColor = bg;
3847
+ function getCellText(cellContent) {
3848
+ const before = segments.length;
3849
+ processContent(cellContent);
3850
+ const cellSegs = segments.splice(before);
3851
+ return cellSegs.map((s) => s.text.replace(/\n$/g, "")).join(" ").replace(/\|/g, "\\|").trim();
3852
+ }
3853
+ function processContent(content) {
3854
+ for (const element of content) {
3855
+ if (element.paragraph?.elements) {
3856
+ for (const textElement of element.paragraph.elements) {
3857
+ if (textElement.textRun?.content && textElement.startIndex != null && textElement.endIndex != null) {
3858
+ const seg = {
3859
+ text: textElement.textRun.content,
3860
+ startIndex: textElement.startIndex,
3861
+ endIndex: textElement.endIndex
3862
+ };
3863
+ if (withFormatting) {
3864
+ const ts = textElement.textRun.textStyle;
3865
+ if (ts) {
3866
+ if (ts.weightedFontFamily?.fontFamily) seg.fontFamily = ts.weightedFontFamily.fontFamily;
3867
+ if (ts.fontSize?.magnitude != null) seg.fontSize = ts.fontSize.magnitude;
3868
+ if (ts.bold) seg.bold = true;
3869
+ if (ts.italic) seg.italic = true;
3870
+ if (ts.underline) seg.underline = true;
3871
+ if (ts.strikethrough) seg.strikethrough = true;
3872
+ const fg = rgbColorToHex(ts.foregroundColor);
3873
+ const bg = rgbColorToHex(ts.backgroundColor);
3874
+ if (fg) seg.foregroundColor = fg;
3875
+ if (bg) seg.backgroundColor = bg;
3876
+ }
3877
+ }
3878
+ segments.push(seg);
3879
+ } else {
3880
+ const inlineText = resolveInlineElementText2(textElement, inlineObjects);
3881
+ if (inlineText && textElement.startIndex != null && textElement.endIndex != null) {
3882
+ segments.push({
3883
+ text: inlineText,
3884
+ startIndex: textElement.startIndex,
3885
+ endIndex: textElement.endIndex
3886
+ });
3617
3887
  }
3618
3888
  }
3619
- segments.push(seg);
3620
3889
  }
3890
+ } else if (element.table?.tableRows) {
3891
+ const rows = [];
3892
+ for (let rowIdx = 0; rowIdx < element.table.tableRows.length; rowIdx++) {
3893
+ const row = element.table.tableRows[rowIdx];
3894
+ if (!row.tableCells) continue;
3895
+ const cellTexts = [];
3896
+ for (const cell of row.tableCells) {
3897
+ cellTexts.push(cell.content ? getCellText(cell.content) : "");
3898
+ }
3899
+ rows.push("| " + cellTexts.join(" | ") + " |");
3900
+ if (rowIdx === 0) {
3901
+ rows.push("| " + cellTexts.map(() => "---").join(" | ") + " |");
3902
+ }
3903
+ }
3904
+ const md = rows.join("\n") + "\n\n";
3905
+ if (element.startIndex != null && element.endIndex != null) {
3906
+ segments.push({
3907
+ text: md,
3908
+ startIndex: element.startIndex,
3909
+ endIndex: element.endIndex
3910
+ });
3911
+ }
3912
+ } else if (element.tableOfContents?.content) {
3913
+ processContent(element.tableOfContents.content);
3621
3914
  }
3622
3915
  }
3623
3916
  }
3917
+ processContent(bodyContent);
3624
3918
  return segments;
3625
3919
  }, formatSegments2 = function(segments) {
3626
3920
  let result = "";
@@ -3677,7 +3971,7 @@ Link: ${doc.webViewLink}` }],
3677
3971
  }
3678
3972
  }
3679
3973
  };
3680
- var extractSegments = extractSegments2, formatSegments = formatSegments2, hasFormattingInfo = hasFormattingInfo2, buildMetaLine = buildMetaLine2, trackFonts = trackFonts2;
3974
+ var resolveInlineElementText = resolveInlineElementText2, extractSegments = extractSegments2, formatSegments = formatSegments2, hasFormattingInfo = hasFormattingInfo2, buildMetaLine = buildMetaLine2, trackFonts = trackFonts2;
3681
3975
  const validation = GetGoogleDocContentSchema.safeParse(args);
3682
3976
  if (!validation.success) {
3683
3977
  return errorResponse(validation.error.errors[0].message);
@@ -3705,7 +3999,8 @@ Link: ${doc.webViewLink}` }],
3705
3999
  `;
3706
4000
  }
3707
4001
  if (bodyContent) {
3708
- const segments = extractSegments2(bodyContent);
4002
+ const tabInlineObjects = tab.documentTab?.inlineObjects;
4003
+ const segments = extractSegments2(bodyContent, tabInlineObjects);
3709
4004
  trackFonts2(segments);
3710
4005
  formattedContent += formatSegments2(segments);
3711
4006
  if (segments.length > 0) {
@@ -3719,7 +4014,8 @@ Link: ${doc.webViewLink}` }],
3719
4014
  } else {
3720
4015
  const bodyContent = document.data.body?.content;
3721
4016
  if (bodyContent) {
3722
- const segments = extractSegments2(bodyContent);
4017
+ const legacyInlineObjects = document.data.inlineObjects;
4018
+ const segments = extractSegments2(bodyContent, legacyInlineObjects);
3723
4019
  trackFonts2(segments);
3724
4020
  formattedContent += formatSegments2(segments);
3725
4021
  totalLength = segments.length > 0 ? segments[segments.length - 1].endIndex : 0;
@@ -4803,6 +5099,47 @@ Image URL: ${imageUrl}` }],
4803
5099
  }
4804
5100
  return { content: [{ type: "text", text: hits.length ? hits.join("\n") : "No smart chips detected (note: only the default tab is scanned)." }], isError: false };
4805
5101
  }
5102
+ case "createFootnote": {
5103
+ const validation = CreateFootnoteSchema.safeParse(args);
5104
+ if (!validation.success) return errorResponse(validation.error.errors[0].message);
5105
+ const a = validation.data;
5106
+ const docs = ctx.google.docs({ version: "v1", auth: ctx.authClient });
5107
+ const createFootnoteReq = {};
5108
+ if (a.index !== void 0) {
5109
+ createFootnoteReq.location = { index: a.index };
5110
+ } else {
5111
+ createFootnoteReq.endOfSegmentLocation = { segmentId: "" };
5112
+ }
5113
+ const res = await docs.documents.batchUpdate({
5114
+ documentId: a.documentId,
5115
+ requestBody: {
5116
+ requests: [{ createFootnote: createFootnoteReq }]
5117
+ }
5118
+ });
5119
+ const footnoteId = res.data.replies?.[0]?.createFootnote?.footnoteId;
5120
+ if (!footnoteId) {
5121
+ return errorResponse("Failed to create footnote \u2014 no footnoteId in response.");
5122
+ }
5123
+ const locationDesc = a.index !== void 0 ? `at index ${a.index}` : "at end of document";
5124
+ if (a.content) {
5125
+ try {
5126
+ await docs.documents.batchUpdate({
5127
+ documentId: a.documentId,
5128
+ requestBody: {
5129
+ requests: [{
5130
+ insertText: {
5131
+ location: { segmentId: footnoteId, index: 0 },
5132
+ text: a.content
5133
+ }
5134
+ }]
5135
+ }
5136
+ });
5137
+ } catch (err) {
5138
+ return { content: [{ type: "text", text: `Created footnote ${footnoteId} ${locationDesc}, but failed to insert content: ${err.message}` }], isError: true };
5139
+ }
5140
+ }
5141
+ return { content: [{ type: "text", text: `Created footnote ${footnoteId} ${locationDesc}.${a.content ? " Content inserted." : ""}` }], isError: false };
5142
+ }
4806
5143
  default:
4807
5144
  return null;
4808
5145
  }