@triffon/google-docs-mcp 1.11.3-triffon

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.
Files changed (165) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +567 -0
  3. package/dist/auth.js +327 -0
  4. package/dist/cachedToolsList.js +38 -0
  5. package/dist/clients.js +180 -0
  6. package/dist/config.js +9 -0
  7. package/dist/downloadProxy.js +82 -0
  8. package/dist/driveQueryUtils.js +7 -0
  9. package/dist/firestoreTokenStorage.js +32 -0
  10. package/dist/googleDocsApiHelpers.js +1257 -0
  11. package/dist/googleSheetsApiHelpers.js +849 -0
  12. package/dist/index.js +240 -0
  13. package/dist/landingPage.js +103 -0
  14. package/dist/logger.js +58 -0
  15. package/dist/markdown-transformer/docsToMarkdown.js +259 -0
  16. package/dist/markdown-transformer/index.js +128 -0
  17. package/dist/markdown-transformer/markdownToDocs.js +834 -0
  18. package/dist/remoteWrapper.js +72 -0
  19. package/dist/tools/calendar/createEvent.js +89 -0
  20. package/dist/tools/calendar/deleteEvent.js +49 -0
  21. package/dist/tools/calendar/helpers.js +19 -0
  22. package/dist/tools/calendar/index.js +12 -0
  23. package/dist/tools/calendar/listEvents.js +84 -0
  24. package/dist/tools/calendar/quickAddEvent.js +55 -0
  25. package/dist/tools/calendar/updateEvent.js +84 -0
  26. package/dist/tools/docs/addTab.js +84 -0
  27. package/dist/tools/docs/appendTableRows.js +89 -0
  28. package/dist/tools/docs/appendToGoogleDoc.js +85 -0
  29. package/dist/tools/docs/cloneTable.js +159 -0
  30. package/dist/tools/docs/comments/addComment.js +83 -0
  31. package/dist/tools/docs/comments/deleteComment.js +30 -0
  32. package/dist/tools/docs/comments/getComment.js +45 -0
  33. package/dist/tools/docs/comments/index.js +14 -0
  34. package/dist/tools/docs/comments/listComments.js +43 -0
  35. package/dist/tools/docs/comments/replyToComment.js +35 -0
  36. package/dist/tools/docs/comments/resolveComment.js +55 -0
  37. package/dist/tools/docs/deleteRange.js +61 -0
  38. package/dist/tools/docs/deleteTableRows.js +62 -0
  39. package/dist/tools/docs/findAndReplace.js +54 -0
  40. package/dist/tools/docs/findElement.js +43 -0
  41. package/dist/tools/docs/findSectionsByHeading.js +46 -0
  42. package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
  43. package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
  44. package/dist/tools/docs/formatting/batchApplyTextStyle.js +86 -0
  45. package/dist/tools/docs/formatting/index.js +16 -0
  46. package/dist/tools/docs/formatting/updateTableBorders.js +81 -0
  47. package/dist/tools/docs/formatting/updateTableCellStyle.js +85 -0
  48. package/dist/tools/docs/formatting/updateTableColumnWidth.js +51 -0
  49. package/dist/tools/docs/formatting/updateTableRowStyle.js +74 -0
  50. package/dist/tools/docs/getTableStructure.js +48 -0
  51. package/dist/tools/docs/index.js +66 -0
  52. package/dist/tools/docs/insertDateChip.js +82 -0
  53. package/dist/tools/docs/insertImage.js +112 -0
  54. package/dist/tools/docs/insertPageBreak.js +47 -0
  55. package/dist/tools/docs/insertPerson.js +53 -0
  56. package/dist/tools/docs/insertRichLink.js +58 -0
  57. package/dist/tools/docs/insertSectionBreak.js +60 -0
  58. package/dist/tools/docs/insertTable.js +42 -0
  59. package/dist/tools/docs/insertTableWithData.js +125 -0
  60. package/dist/tools/docs/insertText.js +49 -0
  61. package/dist/tools/docs/listDocumentTables.js +47 -0
  62. package/dist/tools/docs/listDocumentTabs.js +59 -0
  63. package/dist/tools/docs/listSmartChips.js +41 -0
  64. package/dist/tools/docs/modifyText.js +147 -0
  65. package/dist/tools/docs/readGoogleDoc.js +164 -0
  66. package/dist/tools/docs/renameTab.js +47 -0
  67. package/dist/tools/docs/replaceTableRowData.js +55 -0
  68. package/dist/tools/docs/smartChipHelpers.js +71 -0
  69. package/dist/tools/docs/structureHelpers.js +250 -0
  70. package/dist/tools/docs/tabFieldMasks.js +48 -0
  71. package/dist/tools/docs/tableRowDataHelpers.js +53 -0
  72. package/dist/tools/docs/updateSectionStyle.js +148 -0
  73. package/dist/tools/drive/copyFile.js +63 -0
  74. package/dist/tools/drive/createDocument.js +105 -0
  75. package/dist/tools/drive/createFolder.js +48 -0
  76. package/dist/tools/drive/createFromTemplate.js +82 -0
  77. package/dist/tools/drive/deleteFile.js +72 -0
  78. package/dist/tools/drive/downloadFile.js +266 -0
  79. package/dist/tools/drive/getDocumentInfo.js +48 -0
  80. package/dist/tools/drive/getFolderInfo.js +48 -0
  81. package/dist/tools/drive/index.js +34 -0
  82. package/dist/tools/drive/listDriveFiles.js +129 -0
  83. package/dist/tools/drive/listFolderContents.js +83 -0
  84. package/dist/tools/drive/listGoogleDocs.js +70 -0
  85. package/dist/tools/drive/moveFile.js +54 -0
  86. package/dist/tools/drive/renameFile.js +39 -0
  87. package/dist/tools/drive/savePathGuard.js +86 -0
  88. package/dist/tools/drive/searchDriveFiles.js +148 -0
  89. package/dist/tools/drive/searchGoogleDocs.js +77 -0
  90. package/dist/tools/drive/setFilePermission.js +68 -0
  91. package/dist/tools/gmail/createDraft.js +58 -0
  92. package/dist/tools/gmail/deleteDraft.js +37 -0
  93. package/dist/tools/gmail/getDraft.js +52 -0
  94. package/dist/tools/gmail/getMessage.js +92 -0
  95. package/dist/tools/gmail/helpers.js +113 -0
  96. package/dist/tools/gmail/index.js +28 -0
  97. package/dist/tools/gmail/listDrafts.js +74 -0
  98. package/dist/tools/gmail/listLabels.js +31 -0
  99. package/dist/tools/gmail/listMessages.js +87 -0
  100. package/dist/tools/gmail/modifyMessageLabels.js +54 -0
  101. package/dist/tools/gmail/sendDraft.js +42 -0
  102. package/dist/tools/gmail/sendEmail.js +56 -0
  103. package/dist/tools/gmail/trashMessage.js +37 -0
  104. package/dist/tools/gmail/triageInbox.js +147 -0
  105. package/dist/tools/gmail/updateDraft.js +60 -0
  106. package/dist/tools/index.js +64 -0
  107. package/dist/tools/script/appsScriptShared.js +96 -0
  108. package/dist/tools/script/createAppsScriptProject.js +74 -0
  109. package/dist/tools/script/getAppsScriptContent.js +56 -0
  110. package/dist/tools/script/index.js +14 -0
  111. package/dist/tools/script/updateAppsScriptContent.js +56 -0
  112. package/dist/tools/sheets/addConditionalFormatting.js +143 -0
  113. package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
  114. package/dist/tools/sheets/appendSpreadsheetRows.js +44 -0
  115. package/dist/tools/sheets/appendTableRows.js +51 -0
  116. package/dist/tools/sheets/autoResizeColumns.js +67 -0
  117. package/dist/tools/sheets/autoResizeRows.js +63 -0
  118. package/dist/tools/sheets/batchWrite.js +61 -0
  119. package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
  120. package/dist/tools/sheets/comments/commentAnchor.js +95 -0
  121. package/dist/tools/sheets/comments/createSheetsCellNote.js +33 -0
  122. package/dist/tools/sheets/comments/createSheetsComment.js +168 -0
  123. package/dist/tools/sheets/comments/deleteSheetsComment.js +32 -0
  124. package/dist/tools/sheets/comments/getSheetsComment.js +51 -0
  125. package/dist/tools/sheets/comments/index.js +16 -0
  126. package/dist/tools/sheets/comments/listSheetsComments.js +177 -0
  127. package/dist/tools/sheets/comments/replyToSheetsComment.js +37 -0
  128. package/dist/tools/sheets/comments/resolveSheetsComment.js +53 -0
  129. package/dist/tools/sheets/copyFormatting.js +59 -0
  130. package/dist/tools/sheets/copySheetTo.js +36 -0
  131. package/dist/tools/sheets/createSpreadsheet.js +72 -0
  132. package/dist/tools/sheets/createTable.js +120 -0
  133. package/dist/tools/sheets/deleteChart.js +41 -0
  134. package/dist/tools/sheets/deleteConditionalFormatting.js +46 -0
  135. package/dist/tools/sheets/deleteSheet.js +43 -0
  136. package/dist/tools/sheets/deleteTable.js +56 -0
  137. package/dist/tools/sheets/duplicateSheet.js +53 -0
  138. package/dist/tools/sheets/formatCells.js +122 -0
  139. package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
  140. package/dist/tools/sheets/getConditionalFormatting.js +98 -0
  141. package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
  142. package/dist/tools/sheets/getTable.js +48 -0
  143. package/dist/tools/sheets/groupRows.js +62 -0
  144. package/dist/tools/sheets/index.js +84 -0
  145. package/dist/tools/sheets/insertChart.js +225 -0
  146. package/dist/tools/sheets/listGoogleSheets.js +66 -0
  147. package/dist/tools/sheets/listTables.js +55 -0
  148. package/dist/tools/sheets/protectRange.js +59 -0
  149. package/dist/tools/sheets/readCellFormat.js +143 -0
  150. package/dist/tools/sheets/readSpreadsheet.js +36 -0
  151. package/dist/tools/sheets/renameSheet.js +48 -0
  152. package/dist/tools/sheets/setCellBorders.js +94 -0
  153. package/dist/tools/sheets/setColumnWidths.js +43 -0
  154. package/dist/tools/sheets/setDropdownValidation.js +51 -0
  155. package/dist/tools/sheets/setRowHeights.js +63 -0
  156. package/dist/tools/sheets/ungroupAllRows.js +66 -0
  157. package/dist/tools/sheets/updateTableRange.js +51 -0
  158. package/dist/tools/sheets/writeSpreadsheet.js +45 -0
  159. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +95 -0
  160. package/dist/tools/utils/index.js +8 -0
  161. package/dist/tools/utils/replaceDocumentWithMarkdown.js +161 -0
  162. package/dist/tools/utils/replaceRangeWithMarkdown.js +72 -0
  163. package/dist/types.js +208 -0
  164. package/dist/upstreamAuth.js +62 -0
  165. package/package.json +51 -0
@@ -0,0 +1,53 @@
1
+ import { UserError } from 'fastmcp';
2
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
3
+ export function buildReplaceTableCellContentRequests(cell, nextValue, tabId) {
4
+ const requests = [];
5
+ const insertionIndex = cell.contentStartIndex ?? cell.startIndex;
6
+ if (insertionIndex == null) {
7
+ throw new UserError(`Cell [row=${cell.rowIndex}, col=${cell.columnIndex}] does not have a writable insertion index.`);
8
+ }
9
+ if (cell.text &&
10
+ cell.contentStartIndex !== null &&
11
+ cell.contentEndIndex !== null &&
12
+ cell.contentEndIndex - 1 > cell.contentStartIndex) {
13
+ const range = {
14
+ startIndex: cell.contentStartIndex,
15
+ endIndex: cell.contentEndIndex - 1,
16
+ };
17
+ if (tabId)
18
+ range.tabId = tabId;
19
+ requests.push({
20
+ deleteContentRange: { range: range },
21
+ });
22
+ }
23
+ if (nextValue) {
24
+ const location = { index: insertionIndex };
25
+ if (tabId)
26
+ location.tabId = tabId;
27
+ requests.push({
28
+ insertText: {
29
+ location: location,
30
+ text: nextValue,
31
+ },
32
+ });
33
+ }
34
+ return requests;
35
+ }
36
+ export function buildReplaceTableRowRequests(table, rowIndex, values, tabId) {
37
+ if (rowIndex < 0 || rowIndex >= table.rowCount) {
38
+ throw new UserError(`Row index ${rowIndex} is out of bounds for table ${table.tableId} with ${table.rowCount} rows.`);
39
+ }
40
+ if (values.length > table.columnCount) {
41
+ throw new UserError(`Received ${values.length} values for table ${table.tableId}, but the table only has ${table.columnCount} columns.`);
42
+ }
43
+ return table.cells
44
+ .filter((cell) => cell.rowIndex === rowIndex)
45
+ .sort((a, b) => b.columnIndex - a.columnIndex)
46
+ .flatMap((cell) => buildReplaceTableCellContentRequests(cell, values[cell.columnIndex] ?? '', tabId));
47
+ }
48
+ export async function replaceTableRowData(docs, documentId, table, rowIndex, values, tabId) {
49
+ const requests = buildReplaceTableRowRequests(table, rowIndex, values, tabId);
50
+ if (requests.length === 0)
51
+ return;
52
+ await GDocsHelpers.executeBatchUpdate(docs, documentId, requests);
53
+ }
@@ -0,0 +1,148 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient } from '../../clients.js';
4
+ import { DocumentIdParameter } from '../../types.js';
5
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
6
+ export function buildUpdateSectionStyleRequest(params) {
7
+ const sectionStyle = {};
8
+ const fields = [];
9
+ if (params.flipPageOrientation !== undefined) {
10
+ sectionStyle.flipPageOrientation = params.flipPageOrientation;
11
+ fields.push('flipPageOrientation');
12
+ }
13
+ if (params.sectionType !== undefined) {
14
+ sectionStyle.sectionType = params.sectionType;
15
+ fields.push('sectionType');
16
+ }
17
+ if (params.marginTop !== undefined) {
18
+ sectionStyle.marginTop = { magnitude: params.marginTop, unit: 'PT' };
19
+ fields.push('marginTop');
20
+ }
21
+ if (params.marginBottom !== undefined) {
22
+ sectionStyle.marginBottom = { magnitude: params.marginBottom, unit: 'PT' };
23
+ fields.push('marginBottom');
24
+ }
25
+ if (params.marginLeft !== undefined) {
26
+ sectionStyle.marginLeft = { magnitude: params.marginLeft, unit: 'PT' };
27
+ fields.push('marginLeft');
28
+ }
29
+ if (params.marginRight !== undefined) {
30
+ sectionStyle.marginRight = { magnitude: params.marginRight, unit: 'PT' };
31
+ fields.push('marginRight');
32
+ }
33
+ if (params.pageNumberStart !== undefined) {
34
+ sectionStyle.pageNumberStart = params.pageNumberStart;
35
+ fields.push('pageNumberStart');
36
+ }
37
+ if (fields.length === 0) {
38
+ return null;
39
+ }
40
+ const range = {
41
+ startIndex: params.startIndex,
42
+ endIndex: params.endIndex,
43
+ };
44
+ if (params.tabId) {
45
+ range.tabId = params.tabId;
46
+ }
47
+ return {
48
+ request: {
49
+ updateSectionStyle: {
50
+ range,
51
+ sectionStyle,
52
+ fields: fields.join(','),
53
+ },
54
+ },
55
+ fields,
56
+ };
57
+ }
58
+ export function register(server) {
59
+ server.addTool({
60
+ name: 'updateSectionStyle',
61
+ description: 'Updates the style of a section identified by a character range. The range must cover the section whose style is changing — typically from the character right after an inserted section break to any index inside that section. Supports flipping page orientation (landscape <-> portrait), setting margins, and changing the section type. Common workflow for a landscape page: call insertSectionBreak with sectionType="NEXT_PAGE" before the landscape content, call insertSectionBreak again after it, then call updateSectionStyle on the range between the two breaks with flipPageOrientation=true.',
62
+ parameters: DocumentIdParameter.extend({
63
+ startIndex: z
64
+ .number()
65
+ .int()
66
+ .min(1)
67
+ .describe('The starting index of the range covering the section to style (inclusive, 1-based). Typically the index immediately after the section break that opened this section.'),
68
+ endIndex: z
69
+ .number()
70
+ .int()
71
+ .min(1)
72
+ .describe('The ending index of the range covering the section to style (exclusive). Any index inside the section works — the style applies to the whole section.'),
73
+ flipPageOrientation: z
74
+ .boolean()
75
+ .optional()
76
+ .describe('If true, swaps the section page width and height (effectively toggling between portrait and landscape). If false, keeps the default document orientation.'),
77
+ sectionType: z
78
+ .enum(['SECTION_TYPE_UNSPECIFIED', 'CONTINUOUS', 'NEXT_PAGE'])
79
+ .optional()
80
+ .describe('Changes the section type after the break. Usually set at insert time via insertSectionBreak, but can be updated here.'),
81
+ marginTop: z
82
+ .number()
83
+ .nonnegative()
84
+ .optional()
85
+ .describe('Top margin of the section in points (1 inch = 72 points).'),
86
+ marginBottom: z
87
+ .number()
88
+ .nonnegative()
89
+ .optional()
90
+ .describe('Bottom margin of the section in points.'),
91
+ marginLeft: z
92
+ .number()
93
+ .nonnegative()
94
+ .optional()
95
+ .describe('Left margin of the section in points.'),
96
+ marginRight: z
97
+ .number()
98
+ .nonnegative()
99
+ .optional()
100
+ .describe('Right margin of the section in points.'),
101
+ pageNumberStart: z
102
+ .number()
103
+ .int()
104
+ .min(1)
105
+ .optional()
106
+ .describe('Page number to start the section from. If unset, numbering continues from the previous section.'),
107
+ tabId: z
108
+ .string()
109
+ .optional()
110
+ .describe('The ID of the specific tab to update. Use listDocumentTabs to get tab IDs. If not specified, targets the first tab.'),
111
+ }).refine((data) => data.endIndex > data.startIndex, {
112
+ message: 'endIndex must be greater than startIndex',
113
+ path: ['endIndex'],
114
+ }),
115
+ execute: async (args, { log }) => {
116
+ const docs = await getDocsClient();
117
+ log.info(`Updating section style in doc ${args.documentId} for range ${args.startIndex}-${args.endIndex}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
118
+ try {
119
+ if (args.tabId) {
120
+ const targetTab = await GDocsHelpers.getDocumentTab(docs, args.documentId, args.tabId);
121
+ }
122
+ const built = buildUpdateSectionStyleRequest({
123
+ startIndex: args.startIndex,
124
+ endIndex: args.endIndex,
125
+ flipPageOrientation: args.flipPageOrientation,
126
+ sectionType: args.sectionType,
127
+ marginTop: args.marginTop,
128
+ marginBottom: args.marginBottom,
129
+ marginLeft: args.marginLeft,
130
+ marginRight: args.marginRight,
131
+ pageNumberStart: args.pageNumberStart,
132
+ tabId: args.tabId,
133
+ });
134
+ if (!built) {
135
+ throw new UserError('No section style options were provided. Set at least one of: flipPageOrientation, sectionType, marginTop, marginBottom, marginLeft, marginRight, pageNumberStart.');
136
+ }
137
+ await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [built.request]);
138
+ return `Successfully updated section style (${built.fields.join(', ')}) for range ${args.startIndex}-${args.endIndex}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
139
+ }
140
+ catch (error) {
141
+ log.error(`Error updating section style in doc ${args.documentId}: ${error.message || error}`);
142
+ if (error instanceof UserError)
143
+ throw error;
144
+ throw new UserError(`Failed to update section style: ${error.message || 'Unknown error'}`);
145
+ }
146
+ },
147
+ });
148
+ }
@@ -0,0 +1,63 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDriveClient } from '../../clients.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'copyFile',
7
+ description: "Creates a copy of a file or document in Google Drive. Returns the new copy's ID and URL.",
8
+ parameters: z.strictObject({
9
+ fileId: z
10
+ .string()
11
+ .describe('The file or folder ID from a Google Drive URL or a previous tool result.'),
12
+ newName: z
13
+ .string()
14
+ .optional()
15
+ .describe('Name for the copied file. If not provided, will use "Copy of [original name]".'),
16
+ parentFolderId: z
17
+ .string()
18
+ .optional()
19
+ .describe('ID of folder where copy should be placed. If not provided, places in same location as original.'),
20
+ }),
21
+ execute: async (args, { log }) => {
22
+ const drive = await getDriveClient();
23
+ log.info(`Copying file ${args.fileId} ${args.newName ? `as "${args.newName}"` : ''}`);
24
+ try {
25
+ // Get original file info
26
+ const originalFile = await drive.files.get({
27
+ fileId: args.fileId,
28
+ fields: 'name,parents',
29
+ supportsAllDrives: true,
30
+ });
31
+ const copyMetadata = {
32
+ name: args.newName || `Copy of ${originalFile.data.name}`,
33
+ };
34
+ if (args.parentFolderId) {
35
+ copyMetadata.parents = [args.parentFolderId];
36
+ }
37
+ else if (originalFile.data.parents) {
38
+ copyMetadata.parents = originalFile.data.parents;
39
+ }
40
+ const response = await drive.files.copy({
41
+ fileId: args.fileId,
42
+ requestBody: copyMetadata,
43
+ fields: 'id,name,webViewLink',
44
+ supportsAllDrives: true,
45
+ });
46
+ const copiedFile = response.data;
47
+ return JSON.stringify({
48
+ id: copiedFile.id,
49
+ name: copiedFile.name,
50
+ url: copiedFile.webViewLink,
51
+ }, null, 2);
52
+ }
53
+ catch (error) {
54
+ log.error(`Error copying file: ${error.message || error}`);
55
+ if (error.code === 404)
56
+ throw new UserError('Original file or destination folder not found. Check the IDs.');
57
+ if (error.code === 403)
58
+ throw new UserError('Permission denied. Make sure you have read access to the original file and write access to the destination.');
59
+ throw new UserError(`Failed to copy file: ${error.message || 'Unknown error'}`);
60
+ }
61
+ },
62
+ });
63
+ }
@@ -0,0 +1,105 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDriveClient, getDocsClient } from '../../clients.js';
4
+ import { insertMarkdown, formatInsertResult } from '../../markdown-transformer/index.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'createDocument',
8
+ description: 'Creates a new empty Google Document. Optionally places it in a specific folder and adds initial text content.',
9
+ parameters: z.strictObject({
10
+ title: z.string().min(1).describe('Title for the new document.'),
11
+ parentFolderId: z
12
+ .string()
13
+ .optional()
14
+ .describe('ID of folder where document should be created. If not provided, creates in Drive root.'),
15
+ initialContent: z
16
+ .string()
17
+ .optional()
18
+ .describe('Initial content to add to the document. By default, markdown syntax is converted to formatted Google Docs content (headings, bold, italic, links, lists, etc.).'),
19
+ contentFormat: z
20
+ .enum(['markdown', 'raw'])
21
+ .optional()
22
+ .default('markdown')
23
+ .describe("How to interpret initialContent. 'markdown' (default) converts markdown to formatted Google Docs content. 'raw' inserts the text as-is without any conversion."),
24
+ }),
25
+ execute: async (args, { log }) => {
26
+ const drive = await getDriveClient();
27
+ log.info(`Creating new document "${args.title}"`);
28
+ try {
29
+ const documentMetadata = {
30
+ name: args.title,
31
+ mimeType: 'application/vnd.google-apps.document',
32
+ };
33
+ if (args.parentFolderId) {
34
+ documentMetadata.parents = [args.parentFolderId];
35
+ }
36
+ const response = await drive.files.create({
37
+ requestBody: documentMetadata,
38
+ fields: 'id,name,webViewLink',
39
+ supportsAllDrives: true,
40
+ });
41
+ const document = response.data;
42
+ // Add initial content if provided.
43
+ // NOTE: the Drive file already exists at this point, so the doc is created
44
+ // *empty* and content is a second API call. If that second call fails we must
45
+ // NOT report success — doing so silently leaves an empty document. Surface the
46
+ // failure loudly, including the doc id/url so the caller can append to the
47
+ // existing doc instead of creating a duplicate.
48
+ let contentInserted = false;
49
+ if (args.initialContent) {
50
+ try {
51
+ const docs = await getDocsClient();
52
+ if (args.contentFormat === 'raw') {
53
+ await docs.documents.batchUpdate({
54
+ documentId: document.id,
55
+ requestBody: {
56
+ requests: [
57
+ {
58
+ insertText: {
59
+ location: { index: 1 },
60
+ text: args.initialContent,
61
+ },
62
+ },
63
+ ],
64
+ },
65
+ });
66
+ }
67
+ else {
68
+ const result = await insertMarkdown(docs, document.id, args.initialContent, {
69
+ startIndex: 1,
70
+ firstHeadingAsTitle: true,
71
+ });
72
+ log.info(formatInsertResult(result));
73
+ }
74
+ contentInserted = true;
75
+ }
76
+ catch (contentError) {
77
+ const detail = contentError?.message || String(contentError);
78
+ log.error(`Document created but failed to add initial content: ${detail}`);
79
+ throw new UserError(`Document "${document.name}" was created (id: ${document.id}, url: ${document.webViewLink}) ` +
80
+ `but inserting the initial content FAILED, so it is currently EMPTY. ` +
81
+ `Append the content to this existing document (do not create a new one). ` +
82
+ `Underlying error: ${detail}`);
83
+ }
84
+ }
85
+ return JSON.stringify({
86
+ id: document.id,
87
+ name: document.name,
88
+ url: document.webViewLink,
89
+ contentInserted,
90
+ }, null, 2);
91
+ }
92
+ catch (error) {
93
+ // Pass through UserErrors we raised above (e.g. created-but-empty) unchanged.
94
+ if (error instanceof UserError)
95
+ throw error;
96
+ log.error(`Error creating document: ${error.message || error}`);
97
+ if (error.code === 404)
98
+ throw new UserError('Parent folder not found. Check the folder ID.');
99
+ if (error.code === 403)
100
+ throw new UserError('Permission denied. Make sure you have write access to the destination folder.');
101
+ throw new UserError(`Failed to create document: ${error.message || 'Unknown error'}`);
102
+ }
103
+ },
104
+ });
105
+ }
@@ -0,0 +1,48 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDriveClient } from '../../clients.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'createFolder',
7
+ description: 'Creates a new folder in Google Drive. Optionally places it inside an existing parent folder.',
8
+ parameters: z.strictObject({
9
+ name: z.string().min(1).describe('Name for the new folder.'),
10
+ parentFolderId: z
11
+ .string()
12
+ .optional()
13
+ .describe('Parent folder ID. If not provided, creates folder in Drive root.'),
14
+ }),
15
+ execute: async (args, { log }) => {
16
+ const drive = await getDriveClient();
17
+ log.info(`Creating folder "${args.name}" ${args.parentFolderId ? `in parent ${args.parentFolderId}` : 'in root'}`);
18
+ try {
19
+ const folderMetadata = {
20
+ name: args.name,
21
+ mimeType: 'application/vnd.google-apps.folder',
22
+ };
23
+ if (args.parentFolderId) {
24
+ folderMetadata.parents = [args.parentFolderId];
25
+ }
26
+ const response = await drive.files.create({
27
+ requestBody: folderMetadata,
28
+ fields: 'id,name,parents,webViewLink',
29
+ supportsAllDrives: true,
30
+ });
31
+ const folder = response.data;
32
+ return JSON.stringify({
33
+ id: folder.id,
34
+ name: folder.name,
35
+ url: folder.webViewLink,
36
+ }, null, 2);
37
+ }
38
+ catch (error) {
39
+ log.error(`Error creating folder: ${error.message || error}`);
40
+ if (error.code === 404)
41
+ throw new UserError('Parent folder not found. Check the parent folder ID.');
42
+ if (error.code === 403)
43
+ throw new UserError('Permission denied. Make sure you have write access to the parent folder.');
44
+ throw new UserError(`Failed to create folder: ${error.message || 'Unknown error'}`);
45
+ }
46
+ },
47
+ });
48
+ }
@@ -0,0 +1,82 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDriveClient, getDocsClient } from '../../clients.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'createDocumentFromTemplate',
7
+ description: 'Creates a new document by copying an existing template and optionally replacing placeholder text. Provide key-value pairs in the replacements parameter to substitute template variables.',
8
+ parameters: z.strictObject({
9
+ templateId: z.string().describe('ID of the template document to copy from.'),
10
+ newTitle: z.string().min(1).describe('Title for the new document.'),
11
+ parentFolderId: z
12
+ .string()
13
+ .optional()
14
+ .describe('ID of folder where document should be created. If not provided, creates in Drive root.'),
15
+ replacements: z
16
+ .record(z.string())
17
+ .optional()
18
+ .describe('Key-value pairs for text replacements in the template (e.g., {"{{NAME}}": "John Doe", "{{DATE}}": "2024-01-01"}).'),
19
+ }),
20
+ execute: async (args, { log }) => {
21
+ const drive = await getDriveClient();
22
+ log.info(`Creating document from template ${args.templateId} with title "${args.newTitle}"`);
23
+ try {
24
+ // First copy the template
25
+ const copyMetadata = {
26
+ name: args.newTitle,
27
+ };
28
+ if (args.parentFolderId) {
29
+ copyMetadata.parents = [args.parentFolderId];
30
+ }
31
+ const response = await drive.files.copy({
32
+ fileId: args.templateId,
33
+ requestBody: copyMetadata,
34
+ fields: 'id,name,webViewLink',
35
+ supportsAllDrives: true,
36
+ });
37
+ const document = response.data;
38
+ let result = `Successfully created document "${document.name}" from template (ID: ${document.id})\nView Link: ${document.webViewLink}`;
39
+ // Apply text replacements if provided
40
+ if (args.replacements && Object.keys(args.replacements).length > 0) {
41
+ try {
42
+ const docs = await getDocsClient();
43
+ const requests = [];
44
+ // Create replace requests for each replacement
45
+ for (const [searchText, replaceText] of Object.entries(args.replacements)) {
46
+ requests.push({
47
+ replaceAllText: {
48
+ containsText: {
49
+ text: searchText,
50
+ matchCase: false,
51
+ },
52
+ replaceText: replaceText,
53
+ },
54
+ });
55
+ }
56
+ if (requests.length > 0) {
57
+ await docs.documents.batchUpdate({
58
+ documentId: document.id,
59
+ requestBody: { requests },
60
+ });
61
+ const replacementCount = Object.keys(args.replacements).length;
62
+ result += `\n\nApplied ${replacementCount} text replacement${replacementCount !== 1 ? 's' : ''} to the document.`;
63
+ }
64
+ }
65
+ catch (replacementError) {
66
+ log.warn(`Document created but failed to apply replacements: ${replacementError.message}`);
67
+ result += `\n\nDocument created but failed to apply text replacements. You can make changes manually.`;
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+ catch (error) {
73
+ log.error(`Error creating document from template: ${error.message || error}`);
74
+ if (error.code === 404)
75
+ throw new UserError('Template document or parent folder not found. Check the IDs.');
76
+ if (error.code === 403)
77
+ throw new UserError('Permission denied. Make sure you have read access to the template and write access to the destination folder.');
78
+ throw new UserError(`Failed to create document from template: ${error.message || 'Unknown error'}`);
79
+ }
80
+ },
81
+ });
82
+ }
@@ -0,0 +1,72 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDriveClient } from '../../clients.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'deleteFile',
7
+ description: 'Moves a file or folder to the trash, or permanently deletes it. Set permanent=true for irreversible deletion.',
8
+ parameters: z.strictObject({
9
+ fileId: z
10
+ .string()
11
+ .describe('The file or folder ID from a Google Drive URL or a previous tool result.'),
12
+ permanent: z
13
+ .boolean()
14
+ .optional()
15
+ .default(false)
16
+ .describe('If true, permanently deletes the file instead of moving it to trash.'),
17
+ }),
18
+ execute: async (args, { log }) => {
19
+ const drive = await getDriveClient();
20
+ log.info(`Deleting file ${args.fileId} ${args.permanent ? '(permanent)' : '(to trash)'}`);
21
+ try {
22
+ // Get file info before deletion
23
+ const fileInfo = await drive.files.get({
24
+ fileId: args.fileId,
25
+ fields: 'name,mimeType',
26
+ supportsAllDrives: true,
27
+ });
28
+ const fileName = fileInfo.data.name;
29
+ const isFolder = fileInfo.data.mimeType === 'application/vnd.google-apps.folder';
30
+ if (args.permanent) {
31
+ await drive.files.delete({
32
+ fileId: args.fileId,
33
+ supportsAllDrives: true,
34
+ });
35
+ return JSON.stringify({
36
+ success: true,
37
+ action: 'permanently_deleted',
38
+ fileId: args.fileId,
39
+ fileName,
40
+ type: isFolder ? 'folder' : 'file',
41
+ message: `Permanently deleted ${isFolder ? 'folder' : 'file'} "${fileName}".`,
42
+ }, null, 2);
43
+ }
44
+ else {
45
+ await drive.files.update({
46
+ fileId: args.fileId,
47
+ requestBody: {
48
+ trashed: true,
49
+ },
50
+ supportsAllDrives: true,
51
+ });
52
+ return JSON.stringify({
53
+ success: true,
54
+ action: 'trashed',
55
+ fileId: args.fileId,
56
+ fileName,
57
+ type: isFolder ? 'folder' : 'file',
58
+ message: `Moved ${isFolder ? 'folder' : 'file'} "${fileName}" to trash. It can be restored from the trash.`,
59
+ }, null, 2);
60
+ }
61
+ }
62
+ catch (error) {
63
+ log.error(`Error deleting file: ${error.message || error}`);
64
+ if (error.code === 404)
65
+ throw new UserError('File not found. Check the file ID.');
66
+ if (error.code === 403)
67
+ throw new UserError('Permission denied. Make sure you have delete access to this file.');
68
+ throw new UserError(`Failed to delete file: ${error.message || 'Unknown error'}`);
69
+ }
70
+ },
71
+ });
72
+ }