@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,143 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSheetsClient } from '../../clients.js';
4
+ import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
5
+ const ONE_VALUE_CONDITIONS = [
6
+ 'NUMBER_GREATER',
7
+ 'NUMBER_GREATER_THAN_EQ',
8
+ 'NUMBER_LESS',
9
+ 'NUMBER_LESS_THAN_EQ',
10
+ 'NUMBER_EQ',
11
+ 'NUMBER_NOT_EQ',
12
+ 'CUSTOM_FORMULA',
13
+ ];
14
+ const TWO_VALUE_CONDITIONS = ['NUMBER_BETWEEN', 'NUMBER_NOT_BETWEEN'];
15
+ const NO_VALUE_CONDITIONS = ['BLANK', 'NOT_BLANK'];
16
+ const ALL_CONDITION_TYPES = [
17
+ ...ONE_VALUE_CONDITIONS,
18
+ ...TWO_VALUE_CONDITIONS,
19
+ ...NO_VALUE_CONDITIONS,
20
+ ];
21
+ export function register(server) {
22
+ server.addTool({
23
+ name: 'addConditionalFormatting',
24
+ description: 'Adds a conditional formatting rule to one or more ranges in a spreadsheet. Applies a format (background color, bold, text color, etc.) when cells meet a specified condition. Use CUSTOM_FORMULA for complex conditions like "=$A1>$B1". Note: each call appends a new rule — use deleteConditionalFormatting to remove existing rules before re-adding.',
25
+ parameters: z
26
+ .object({
27
+ spreadsheetId: z
28
+ .string()
29
+ .describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
30
+ sheetName: z
31
+ .string()
32
+ .optional()
33
+ .describe('Name of the sheet/tab. Defaults to the first sheet if not provided.'),
34
+ ranges: z
35
+ .array(z.string())
36
+ .min(1)
37
+ .describe('One or more A1 notation ranges the rule applies to (e.g., ["B2:S68"] or ["A1:A10", "C1:C10"]).'),
38
+ conditionType: z
39
+ .enum(ALL_CONDITION_TYPES)
40
+ .describe('The condition type. ' +
41
+ 'NUMBER_* types compare cell values numerically. ' +
42
+ 'CUSTOM_FORMULA evaluates a formula (e.g., "=$A1>10"). ' +
43
+ 'BLANK/NOT_BLANK check whether a cell is empty.'),
44
+ conditionValues: z
45
+ .array(z.string())
46
+ .optional()
47
+ .describe('Values for the condition. ' +
48
+ 'Omit or pass [] for BLANK and NOT_BLANK. ' +
49
+ 'Pass one value for all NUMBER_* single-operand types and CUSTOM_FORMULA. ' +
50
+ 'Pass two values for NUMBER_BETWEEN and NUMBER_NOT_BETWEEN (lower bound first).'),
51
+ backgroundColor: z
52
+ .string()
53
+ .optional()
54
+ .describe('Cell background color as hex (e.g., "#FF9900").'),
55
+ bold: z.boolean().optional().describe('Apply bold text formatting.'),
56
+ italic: z.boolean().optional().describe('Apply italic text formatting.'),
57
+ strikethrough: z.boolean().optional().describe('Apply strikethrough text formatting.'),
58
+ underline: z.boolean().optional().describe('Apply underline text formatting.'),
59
+ foregroundColor: z
60
+ .string()
61
+ .optional()
62
+ .describe('Text (foreground) color as hex (e.g., "#FF0000").'),
63
+ fontSize: z.number().min(1).optional().describe('Font size in points.'),
64
+ })
65
+ .refine((data) => data.backgroundColor !== undefined ||
66
+ data.bold !== undefined ||
67
+ data.italic !== undefined ||
68
+ data.strikethrough !== undefined ||
69
+ data.underline !== undefined ||
70
+ data.foregroundColor !== undefined ||
71
+ data.fontSize !== undefined, { message: 'At least one formatting option must be provided.' })
72
+ .refine((data) => {
73
+ const values = data.conditionValues ?? [];
74
+ if (NO_VALUE_CONDITIONS.includes(data.conditionType)) {
75
+ return values.length === 0;
76
+ }
77
+ if (TWO_VALUE_CONDITIONS.includes(data.conditionType)) {
78
+ return values.length === 2;
79
+ }
80
+ return values.length === 1;
81
+ }, (data) => {
82
+ if (NO_VALUE_CONDITIONS.includes(data.conditionType)) {
83
+ return { message: `${data.conditionType} does not accept condition values.` };
84
+ }
85
+ if (TWO_VALUE_CONDITIONS.includes(data.conditionType)) {
86
+ return { message: `${data.conditionType} requires exactly two condition values.` };
87
+ }
88
+ return { message: `${data.conditionType} requires exactly one condition value.` };
89
+ }),
90
+ execute: async (args, { log }) => {
91
+ const sheets = await getSheetsClient();
92
+ log.info(`Adding conditional format rule to spreadsheet ${args.spreadsheetId}`);
93
+ try {
94
+ const sheetId = await SheetsHelpers.resolveSheetId(sheets, args.spreadsheetId, args.sheetName);
95
+ const gridRanges = args.ranges.map((r) => SheetsHelpers.parseA1ToGridRange(r, sheetId));
96
+ const conditionValues = (args.conditionValues ?? []).map((v) => ({
97
+ userEnteredValue: v,
98
+ }));
99
+ const format = {};
100
+ if (args.backgroundColor) {
101
+ const rgb = SheetsHelpers.hexToRgb(args.backgroundColor);
102
+ if (!rgb)
103
+ throw new UserError(`Invalid background color: "${args.backgroundColor}".`);
104
+ format.backgroundColor = rgb;
105
+ }
106
+ const hasTextFormat = args.bold !== undefined ||
107
+ args.italic !== undefined ||
108
+ args.strikethrough !== undefined ||
109
+ args.underline !== undefined ||
110
+ args.fontSize !== undefined ||
111
+ args.foregroundColor !== undefined;
112
+ if (hasTextFormat) {
113
+ const textFormat = {};
114
+ if (args.bold !== undefined)
115
+ textFormat.bold = args.bold;
116
+ if (args.italic !== undefined)
117
+ textFormat.italic = args.italic;
118
+ if (args.strikethrough !== undefined)
119
+ textFormat.strikethrough = args.strikethrough;
120
+ if (args.underline !== undefined)
121
+ textFormat.underline = args.underline;
122
+ if (args.fontSize !== undefined)
123
+ textFormat.fontSize = args.fontSize;
124
+ if (args.foregroundColor) {
125
+ const rgb = SheetsHelpers.hexToRgb(args.foregroundColor);
126
+ if (!rgb)
127
+ throw new UserError(`Invalid foreground color: "${args.foregroundColor}".`);
128
+ textFormat.foregroundColor = rgb;
129
+ }
130
+ format.textFormat = textFormat;
131
+ }
132
+ await SheetsHelpers.addConditionalFormatRule(sheets, args.spreadsheetId, gridRanges, args.conditionType, conditionValues, format);
133
+ return `Successfully added conditional formatting rule to ${args.ranges.join(', ')}.`;
134
+ }
135
+ catch (error) {
136
+ log.error(`Error adding conditional format rule: ${error.message || error}`);
137
+ if (error instanceof UserError)
138
+ throw error;
139
+ throw new UserError(`Failed to add conditional formatting: ${error.message || 'Unknown error'}`);
140
+ }
141
+ },
142
+ });
143
+ }
@@ -0,0 +1,34 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSheetsClient } from '../../clients.js';
4
+ import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'addSheet',
8
+ description: "Adds a new sheet (tab) to an existing spreadsheet. Returns the new sheet's title and ID.",
9
+ parameters: z.strictObject({
10
+ spreadsheetId: z
11
+ .string()
12
+ .describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
13
+ sheetTitle: z.string().min(1).describe('Title for the new sheet/tab.'),
14
+ }),
15
+ execute: async (args, { log }) => {
16
+ const sheets = await getSheetsClient();
17
+ log.info(`Adding sheet "${args.sheetTitle}" to spreadsheet ${args.spreadsheetId}`);
18
+ try {
19
+ const response = await SheetsHelpers.addSheet(sheets, args.spreadsheetId, args.sheetTitle);
20
+ const addedSheet = response.replies?.[0]?.addSheet?.properties;
21
+ if (!addedSheet) {
22
+ throw new UserError('Failed to add sheet - no sheet properties returned.');
23
+ }
24
+ return `Successfully added sheet "${addedSheet.title}" (Sheet ID: ${addedSheet.sheetId}) to spreadsheet.`;
25
+ }
26
+ catch (error) {
27
+ log.error(`Error adding sheet to spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
28
+ if (error instanceof UserError)
29
+ throw error;
30
+ throw new UserError(`Failed to add sheet: ${error.message || 'Unknown error'}`);
31
+ }
32
+ },
33
+ });
34
+ }
@@ -0,0 +1,44 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSheetsClient } from '../../clients.js';
4
+ import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
5
+ import { SpreadsheetCellValueSchema } from '../../types.js';
6
+ export function register(server) {
7
+ server.addTool({
8
+ name: 'appendRows',
9
+ description: 'Appends rows to the end of a sheet. Data is added after the last row with content in the specified range.',
10
+ parameters: z.strictObject({
11
+ spreadsheetId: z
12
+ .string()
13
+ .describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
14
+ range: z
15
+ .string()
16
+ .describe('A1 notation range indicating where to append (e.g., "A1" or "Sheet1!A1"). Data will be appended starting from this range.'),
17
+ values: z
18
+ .array(z.array(SpreadsheetCellValueSchema))
19
+ .describe('2D array of values to append. Each inner array represents a row.'),
20
+ valueInputOption: z
21
+ .enum(['RAW', 'USER_ENTERED'])
22
+ .optional()
23
+ .default('USER_ENTERED')
24
+ .describe('How input data should be interpreted. RAW: values are stored as-is. USER_ENTERED: values are parsed as if typed by a user.'),
25
+ }),
26
+ execute: async (args, { log }) => {
27
+ const sheets = await getSheetsClient();
28
+ log.info(`Appending rows to spreadsheet ${args.spreadsheetId}, starting at: ${args.range}`);
29
+ try {
30
+ const response = await SheetsHelpers.appendValues(sheets, args.spreadsheetId, args.range, args.values, args.valueInputOption);
31
+ const updatedCells = response.updates?.updatedCells || 0;
32
+ const updatedRows = response.updates?.updatedRows || 0;
33
+ const updatedRange = response.updates?.updatedRange || args.range;
34
+ return `Successfully appended ${updatedRows} row(s) (${updatedCells} cells) to spreadsheet. Updated range: ${updatedRange}`;
35
+ }
36
+ catch (error) {
37
+ log.error(`Error appending to spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
38
+ if (error instanceof UserError)
39
+ throw error;
40
+ throw new UserError(`Failed to append to spreadsheet: ${error.message || 'Unknown error'}`);
41
+ }
42
+ },
43
+ });
44
+ }
@@ -0,0 +1,51 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSheetsClient } from '../../clients.js';
4
+ import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
5
+ import { SpreadsheetCellValueSchema } from '../../types.js';
6
+ export function register(server) {
7
+ server.addTool({
8
+ name: 'appendTableRows',
9
+ description: 'Appends rows to the end of a table using table-aware insertion. This method respects footers and automatically inserts rows before the footer if one exists.',
10
+ parameters: z.strictObject({
11
+ spreadsheetId: z
12
+ .string()
13
+ .describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
14
+ tableIdentifier: z
15
+ .string()
16
+ .describe('The table name or table ID to append rows to. Use listTables to see available tables.'),
17
+ values: z
18
+ .array(z.array(SpreadsheetCellValueSchema))
19
+ .min(1)
20
+ .describe('2D array of values to append. Each inner array represents a row.'),
21
+ valueInputOption: z
22
+ .enum(['RAW', 'USER_ENTERED'])
23
+ .optional()
24
+ .default('USER_ENTERED')
25
+ .describe('How input data should be interpreted. RAW: values are stored as-is. USER_ENTERED (default): values are parsed as if typed by a user.'),
26
+ }),
27
+ execute: async (args, { log }) => {
28
+ const sheets = await getSheetsClient();
29
+ log.info(`Appending ${args.values.length} rows to table "${args.tableIdentifier}"`);
30
+ try {
31
+ // Resolve the table to get its ID
32
+ const { table } = await SheetsHelpers.resolveTableIdentifier(sheets, args.spreadsheetId, args.tableIdentifier);
33
+ // Append rows to the table
34
+ const result = await SheetsHelpers.appendToTableHelper(sheets, args.spreadsheetId, table.tableId || '', args.values);
35
+ return JSON.stringify({
36
+ tableId: table.tableId,
37
+ name: table.name,
38
+ rowsAppended: result.rowsAppended,
39
+ updatedRange: result.updatedRange,
40
+ message: `Successfully appended ${result.rowsAppended} row(s) to table "${table.name}".`,
41
+ }, null, 2);
42
+ }
43
+ catch (error) {
44
+ log.error(`Error appending table rows: ${error.message || error}`);
45
+ if (error instanceof UserError)
46
+ throw error;
47
+ throw new UserError(`Failed to append table rows: ${error.message || 'Unknown error'}`);
48
+ }
49
+ },
50
+ });
51
+ }
@@ -0,0 +1,67 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSheetsClient } from '../../clients.js';
4
+ import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'autoResizeColumns',
8
+ description: 'Auto-resizes columns in a spreadsheet to fit their content. Optionally restrict to a column range (e.g., "A:S"); defaults to all columns.',
9
+ parameters: z.strictObject({
10
+ spreadsheetId: z
11
+ .string()
12
+ .describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
13
+ sheetName: z
14
+ .string()
15
+ .optional()
16
+ .describe('Name of the sheet/tab. Defaults to the first sheet if not provided.'),
17
+ columns: z
18
+ .string()
19
+ .optional()
20
+ .describe('Column range to resize in A1 notation (e.g., "A:S"). Omit to resize all columns.'),
21
+ }),
22
+ execute: async (args, { log }) => {
23
+ const sheets = await getSheetsClient();
24
+ log.info(`Auto-resizing columns in spreadsheet ${args.spreadsheetId}`);
25
+ try {
26
+ const sheetId = await SheetsHelpers.resolveSheetId(sheets, args.spreadsheetId, args.sheetName);
27
+ const dimensionRange = {
28
+ sheetId,
29
+ dimension: 'COLUMNS',
30
+ };
31
+ if (args.columns) {
32
+ const colonIdx = args.columns.indexOf(':');
33
+ if (colonIdx !== -1) {
34
+ dimensionRange.startIndex = SheetsHelpers.colLettersToIndex(args.columns.slice(0, colonIdx).trim());
35
+ dimensionRange.endIndex =
36
+ SheetsHelpers.colLettersToIndex(args.columns.slice(colonIdx + 1).trim()) + 1;
37
+ }
38
+ else {
39
+ const idx = SheetsHelpers.colLettersToIndex(args.columns.trim());
40
+ dimensionRange.startIndex = idx;
41
+ dimensionRange.endIndex = idx + 1;
42
+ }
43
+ }
44
+ await sheets.spreadsheets.batchUpdate({
45
+ spreadsheetId: args.spreadsheetId,
46
+ requestBody: {
47
+ requests: [
48
+ {
49
+ autoResizeDimensions: {
50
+ dimensions: dimensionRange,
51
+ },
52
+ },
53
+ ],
54
+ },
55
+ });
56
+ const rangeDesc = args.columns ? `columns ${args.columns}` : 'all columns';
57
+ return `Successfully auto-resized ${rangeDesc} to fit content.`;
58
+ }
59
+ catch (error) {
60
+ log.error(`Error auto-resizing columns: ${error.message || error}`);
61
+ if (error instanceof UserError)
62
+ throw error;
63
+ throw new UserError(`Failed to auto-resize columns: ${error.message || 'Unknown error'}`);
64
+ }
65
+ },
66
+ });
67
+ }
@@ -0,0 +1,63 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSheetsClient } from '../../clients.js';
4
+ import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'autoResizeRows',
8
+ description: 'Auto-resizes rows in a spreadsheet to fit their content. Optionally restrict to a row range (e.g., startRow=2, endRow=50); defaults to all rows.',
9
+ parameters: z
10
+ .object({
11
+ spreadsheetId: z
12
+ .string()
13
+ .describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
14
+ sheetName: z
15
+ .string()
16
+ .optional()
17
+ .describe('Name of the sheet/tab. Defaults to the first sheet if not provided.'),
18
+ startRow: z
19
+ .number()
20
+ .int()
21
+ .min(1)
22
+ .optional()
23
+ .describe('1-based start row index (inclusive). Omit to start from row 1.'),
24
+ endRow: z
25
+ .number()
26
+ .int()
27
+ .min(1)
28
+ .optional()
29
+ .describe('1-based end row index (inclusive). Omit to resize to the last row.'),
30
+ })
31
+ .refine((d) => d.startRow === undefined || d.endRow === undefined || d.endRow >= d.startRow, {
32
+ message: 'endRow must be greater than or equal to startRow.',
33
+ }),
34
+ execute: async (args, { log }) => {
35
+ const sheets = await getSheetsClient();
36
+ log.info(`Auto-resizing rows in spreadsheet ${args.spreadsheetId}`);
37
+ try {
38
+ const sheetId = await SheetsHelpers.resolveSheetId(sheets, args.spreadsheetId, args.sheetName);
39
+ const dimensionRange = { sheetId, dimension: 'ROWS' };
40
+ if (args.startRow !== undefined)
41
+ dimensionRange.startIndex = args.startRow - 1;
42
+ if (args.endRow !== undefined)
43
+ dimensionRange.endIndex = args.endRow;
44
+ await sheets.spreadsheets.batchUpdate({
45
+ spreadsheetId: args.spreadsheetId,
46
+ requestBody: {
47
+ requests: [{ autoResizeDimensions: { dimensions: dimensionRange } }],
48
+ },
49
+ });
50
+ const rangeDesc = args.startRow !== undefined
51
+ ? `rows ${args.startRow}–${args.endRow ?? 'end'}`
52
+ : 'all rows';
53
+ return `Successfully auto-resized ${rangeDesc} to fit content.`;
54
+ }
55
+ catch (error) {
56
+ log.error(`Error auto-resizing rows: ${error.message || error}`);
57
+ if (error instanceof UserError)
58
+ throw error;
59
+ throw new UserError(`Failed to auto-resize rows: ${error.message || 'Unknown error'}`);
60
+ }
61
+ },
62
+ });
63
+ }
@@ -0,0 +1,61 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSheetsClient } from '../../clients.js';
4
+ import { SpreadsheetCellValueSchema } from '../../types.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'batchWrite',
8
+ description: 'Writes data to multiple ranges in a single API call. More efficient than multiple separate writeSpreadsheet calls when updating several ranges at once.',
9
+ parameters: z.strictObject({
10
+ spreadsheetId: z
11
+ .string()
12
+ .describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
13
+ data: z
14
+ .array(z.strictObject({
15
+ range: z.string().describe('A1 notation range (e.g., "Sheet1!A1:B2").'),
16
+ values: z
17
+ .array(z.array(SpreadsheetCellValueSchema))
18
+ .describe('2D array of values to write. Each inner array represents a row.'),
19
+ }))
20
+ .min(1)
21
+ .max(100)
22
+ .describe('Array of range+values pairs to write in a single batch.'),
23
+ valueInputOption: z
24
+ .enum(['RAW', 'USER_ENTERED'])
25
+ .optional()
26
+ .default('USER_ENTERED')
27
+ .describe('How input data should be interpreted. RAW: values are stored as-is. USER_ENTERED: values are parsed as if typed by a user.'),
28
+ }),
29
+ execute: async (args, { log }) => {
30
+ const sheets = await getSheetsClient();
31
+ const rangeNames = args.data.map((d) => d.range).join(', ');
32
+ log.info(`Batch writing to ${args.data.length} range(s) in spreadsheet ${args.spreadsheetId}: ${rangeNames}`);
33
+ try {
34
+ const response = await sheets.spreadsheets.values.batchUpdate({
35
+ spreadsheetId: args.spreadsheetId,
36
+ requestBody: {
37
+ valueInputOption: args.valueInputOption,
38
+ data: args.data.map((d) => ({ range: d.range, values: d.values })),
39
+ },
40
+ });
41
+ const totalCells = response.data.totalUpdatedCells || 0;
42
+ const totalRows = response.data.totalUpdatedRows || 0;
43
+ const totalColumns = response.data.totalUpdatedColumns || 0;
44
+ const totalSheets = response.data.totalUpdatedSheets || 0;
45
+ return `Successfully batch-wrote ${totalCells} cells (${totalRows} rows, ${totalColumns} columns) across ${totalSheets} sheet(s) in ${args.data.length} range(s).`;
46
+ }
47
+ catch (error) {
48
+ log.error(`Error batch writing to spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
49
+ if (error instanceof UserError)
50
+ throw error;
51
+ if (error.code === 404) {
52
+ throw new UserError(`Spreadsheet not found (ID: ${args.spreadsheetId}). Check the ID.`);
53
+ }
54
+ if (error.code === 403) {
55
+ throw new UserError(`Permission denied for spreadsheet (ID: ${args.spreadsheetId}). Ensure you have write access.`);
56
+ }
57
+ throw new UserError(`Failed to batch write to spreadsheet: ${error.message || 'Unknown error'}`);
58
+ }
59
+ },
60
+ });
61
+ }
@@ -0,0 +1,31 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSheetsClient } from '../../clients.js';
4
+ import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'clearRange',
8
+ description: 'Clears all cell values in a range without deleting the cells themselves. Formatting is preserved.',
9
+ parameters: z.strictObject({
10
+ spreadsheetId: z
11
+ .string()
12
+ .describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
13
+ range: z.string().describe('A1 notation range to clear (e.g., "A1:B10" or "Sheet1!A1:B10").'),
14
+ }),
15
+ execute: async (args, { log }) => {
16
+ const sheets = await getSheetsClient();
17
+ log.info(`Clearing range ${args.range} in spreadsheet ${args.spreadsheetId}`);
18
+ try {
19
+ const response = await SheetsHelpers.clearRange(sheets, args.spreadsheetId, args.range);
20
+ const clearedRange = response.clearedRange || args.range;
21
+ return `Successfully cleared range ${clearedRange}.`;
22
+ }
23
+ catch (error) {
24
+ log.error(`Error clearing range in spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
25
+ if (error instanceof UserError)
26
+ throw error;
27
+ throw new UserError(`Failed to clear range: ${error.message || 'Unknown error'}`);
28
+ }
29
+ },
30
+ });
31
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Shared helpers for resolving the cell location of a spreadsheet comment.
3
+ *
4
+ * A comment can carry its location in two ways:
5
+ * 1. A real Drive `anchor` field (present on native/human-created comments).
6
+ * 2. A click-through deep link in the comment body, of the form
7
+ * `#gid=<sheetId>&range=<A1>`, which is what `createSheetsComment` writes
8
+ * for API-created comments (the Drive API cannot natively anchor
9
+ * spreadsheet comments — see createSheetsComment.ts).
10
+ *
11
+ * Location-based filtering must understand both, otherwise comments created by
12
+ * this server are invisible to cell/row/range filters.
13
+ */
14
+ /**
15
+ * Resolves a comment's cell location, preferring a real Drive anchor and
16
+ * falling back to the deep link embedded in the comment body. Returns null when
17
+ * neither source yields a location.
18
+ */
19
+ export function extractCommentLocation(comment) {
20
+ const fromAnchor = comment.anchor ? parseSheetsAnchor(comment.anchor) : null;
21
+ if (fromAnchor)
22
+ return fromAnchor;
23
+ return comment.content ? parseDeepLink(comment.content) : null;
24
+ }
25
+ /**
26
+ * Parses a Drive Sheets anchor JSON string into a cell location.
27
+ * Anchor shape: `{ a: [{ sht: { sid, rng: { r, c } } }] }`.
28
+ */
29
+ export function parseSheetsAnchor(anchorStr) {
30
+ try {
31
+ const anchor = JSON.parse(anchorStr);
32
+ const actions = anchor.a;
33
+ if (!actions || !Array.isArray(actions))
34
+ return null;
35
+ for (const action of actions) {
36
+ if (action.sht) {
37
+ const sid = action.sht.sid;
38
+ const rng = action.sht.rng;
39
+ if (sid !== undefined && rng) {
40
+ return { sheetId: sid, row: rng.r || 0, col: rng.c || 0 };
41
+ }
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ /**
51
+ * Parses a Sheets click-through deep link of the form
52
+ * `#gid=<sheetId>&range=<A1>` out of free text. When the range spans multiple
53
+ * cells, the top-left cell is used (matching the single-cell filter logic).
54
+ */
55
+ function parseDeepLink(content) {
56
+ const match = content.match(/#gid=(\d+)&range=([^&\s)]+)/);
57
+ if (!match)
58
+ return null;
59
+ const sheetId = Number(match[1]);
60
+ const decoded = decodeURIComponent(match[2]);
61
+ // Strip an optional "Sheet!" prefix, then take the top-left cell of any range.
62
+ const a1Range = decoded.includes('!') ? decoded.slice(decoded.lastIndexOf('!') + 1) : decoded;
63
+ const topLeft = a1Range.split(':')[0];
64
+ const cell = a1ToRowCol(topLeft);
65
+ if (!cell)
66
+ return null;
67
+ return { sheetId, row: cell.row, col: cell.col };
68
+ }
69
+ /**
70
+ * Converts an A1 cell reference (e.g. "B3") to zero-based row/col indices.
71
+ * Returns null for malformed input.
72
+ */
73
+ export function a1ToRowCol(a1) {
74
+ const match = a1.trim().match(/^([A-Za-z]+)(\d+)$/);
75
+ if (!match)
76
+ return null;
77
+ const colStr = match[1].toUpperCase();
78
+ let col = 0;
79
+ for (let i = 0; i < colStr.length; i++) {
80
+ col = col * 26 + (colStr.charCodeAt(i) - 64);
81
+ }
82
+ return { row: parseInt(match[2], 10) - 1, col: col - 1 };
83
+ }
84
+ /**
85
+ * Converts zero-based row/col indices back to an A1 cell reference.
86
+ */
87
+ export function rowColToA1(row, col) {
88
+ let colStr = '';
89
+ let c = col;
90
+ do {
91
+ colStr = String.fromCharCode(65 + (c % 26)) + colStr;
92
+ c = Math.floor(c / 26) - 1;
93
+ } while (c >= 0);
94
+ return `${colStr}${row + 1}`;
95
+ }
@@ -0,0 +1,33 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSheetsClient } from '../../../clients.js';
4
+ import * as SheetsHelpers from '../../../googleSheetsApiHelpers.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'createSheetsCellNote',
8
+ description: 'Creates or replaces a native Google Sheets cell note on a cell or range. Use this when the review text must be attached to a specific cell in the Sheets UI. This is a cell note, not a threaded Drive comment.',
9
+ parameters: z.object({
10
+ spreadsheetId: z
11
+ .string()
12
+ .describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
13
+ range: z
14
+ .string()
15
+ .describe('A1 notation cell or range to attach the note to, e.g. "Sheet1!F94" or "F94".'),
16
+ content: z.string().min(1).describe('The note content to attach to the target cell/range.'),
17
+ }),
18
+ execute: async (args, { log }) => {
19
+ const sheets = await getSheetsClient();
20
+ log.info(`Creating native cell note on ${args.range} in ${args.spreadsheetId}`);
21
+ try {
22
+ await SheetsHelpers.setCellNote(sheets, args.spreadsheetId, args.range, args.content);
23
+ return `Cell note added successfully to range "${args.range}".`;
24
+ }
25
+ catch (error) {
26
+ log.error(`Error creating sheets cell note: ${error.message || error}`);
27
+ if (error instanceof UserError)
28
+ throw error;
29
+ throw new UserError(`Failed to create cell note: ${error.message || 'Unknown error'}`);
30
+ }
31
+ },
32
+ });
33
+ }