@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,168 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { google } from 'googleapis';
4
+ import { getAuthClient, getSheetsClient } from '../../../clients.js';
5
+ const CreateSheetsCommentParameters = z
6
+ .object({
7
+ spreadsheetId: z
8
+ .string()
9
+ .describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
10
+ content: z.string().min(1).describe('The text content of the comment.'),
11
+ sheetName: z
12
+ .string()
13
+ .optional()
14
+ .describe('Optional sheet/tab name for cell or range anchors that do not already include a sheet prefix.'),
15
+ cell: z
16
+ .string()
17
+ .optional()
18
+ .describe('Optional target cell in A1 notation (for example "B3" or "Sheet1!B3"). Used to quote the cell content and build a click-through link to that cell.'),
19
+ range: z
20
+ .string()
21
+ .optional()
22
+ .describe('Optional target range in A1 notation (for example "B3:D5" or "Sheet1!B3:D5"). The click-through link targets the full range.'),
23
+ includeCellLink: z
24
+ .boolean()
25
+ .optional()
26
+ .default(true)
27
+ .describe('When a cell or range is given, appends a direct Google Sheets click-through URL (with gid and range) to the comment content so users can jump to the target cell/range. Defaults to true because the Drive API cannot natively anchor spreadsheet comments in the Sheets UI. Has no effect when no cell/range is provided.'),
28
+ })
29
+ .refine((data) => !(data.cell && data.range), {
30
+ message: 'Provide either cell or range, not both.',
31
+ path: ['range'],
32
+ })
33
+ .refine((data) => {
34
+ if (!data.cell && !data.range) {
35
+ return true;
36
+ }
37
+ if (data.sheetName) {
38
+ return true;
39
+ }
40
+ return hasSheetPrefix(data.cell ?? data.range ?? '');
41
+ }, {
42
+ message: 'sheetName is required when cell or range does not include a sheet prefix.',
43
+ path: ['sheetName'],
44
+ });
45
+ export function register(server) {
46
+ server.addTool({
47
+ name: 'createSheetsComment',
48
+ description: 'Creates a new comment in a Google Spreadsheet. When you target a specific cell or range, a click-through link to that location is added to the comment body by default (includeCellLink), letting users jump to the cell. Note: the Drive API cannot natively anchor spreadsheet comments to a cell in the Sheets UI, so the comment itself appears under "All comments" — the click-through link is the supported way to navigate to the target.',
49
+ parameters: CreateSheetsCommentParameters,
50
+ execute: async (args, { log }) => {
51
+ log.info(`Creating spreadsheet comment in ${args.spreadsheetId}`);
52
+ try {
53
+ const authClient = await getAuthClient();
54
+ const drive = google.drive({ version: 'v3', auth: authClient });
55
+ const sheets = await getSheetsClient();
56
+ let quotedText;
57
+ let locationLabel;
58
+ let cellUrl;
59
+ if (args.cell || args.range) {
60
+ const parsedLocation = parseLocation(args.cell ?? args.range ?? '', args.sheetName);
61
+ const sheetId = await resolveSheetId(sheets, args.spreadsheetId, parsedLocation.sheetName);
62
+ quotedText = await readRangeText(sheets, args.spreadsheetId, `${quoteSheetName(parsedLocation.sheetName)}!${parsedLocation.a1Range}`);
63
+ locationLabel = `${parsedLocation.sheetName}!${parsedLocation.a1Range}`;
64
+ cellUrl = buildCellUrl(args.spreadsheetId, sheetId, parsedLocation.a1Range);
65
+ }
66
+ const content = args.includeCellLink && cellUrl
67
+ ? `${args.content}\n\n→ ${locationLabel}: ${cellUrl}`
68
+ : args.content;
69
+ const response = await drive.comments.create({
70
+ fileId: args.spreadsheetId,
71
+ fields: 'id,quotedFileContent,createdTime',
72
+ requestBody: {
73
+ content,
74
+ ...(quotedText
75
+ ? {
76
+ quotedFileContent: {
77
+ value: quotedText,
78
+ mimeType: 'text/plain',
79
+ },
80
+ }
81
+ : {}),
82
+ },
83
+ });
84
+ const linkNote = args.includeCellLink && cellUrl
85
+ ? ` A click-through link to ${locationLabel} was added to the comment body so users can jump to the target cell.`
86
+ : locationLabel
87
+ ? ` Target location: ${locationLabel}.`
88
+ : '';
89
+ return (`Comment added successfully. Comment ID: ${response.data.id}.` +
90
+ linkNote +
91
+ ' Note: the Drive API cannot natively anchor spreadsheet comments to a cell in the Sheets UI; use the click-through link to navigate to the location.');
92
+ }
93
+ catch (error) {
94
+ log.error(`Error creating sheets comment: ${error.message || error}`);
95
+ const errorDetails = error.response?.data?.error?.message || error.message || 'Unknown error';
96
+ throw new UserError(`Failed to create spreadsheet comment: ${errorDetails}`);
97
+ }
98
+ },
99
+ });
100
+ }
101
+ function buildCellUrl(spreadsheetId, sheetId, a1Range) {
102
+ return `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit#gid=${sheetId}&range=${encodeURIComponent(a1Range)}`;
103
+ }
104
+ function hasSheetPrefix(value) {
105
+ return value.includes('!');
106
+ }
107
+ function parseLocation(value, fallbackSheetName) {
108
+ const bangIndex = value.lastIndexOf('!');
109
+ const rawSheetName = bangIndex >= 0 ? unquoteSheetName(value.slice(0, bangIndex)) : fallbackSheetName;
110
+ const a1Range = bangIndex >= 0 ? value.slice(bangIndex + 1) : value;
111
+ if (!rawSheetName) {
112
+ throw new UserError('Could not determine the target sheet name for the spreadsheet comment.');
113
+ }
114
+ // Validate the start cell is a well-formed A1 reference (fail early on bad input).
115
+ const startCell = a1Range.split(':')[0];
116
+ validateA1Cell(startCell);
117
+ return {
118
+ sheetName: rawSheetName,
119
+ a1Range,
120
+ };
121
+ }
122
+ function validateA1Cell(a1) {
123
+ const match = a1.trim().match(/^([A-Za-z]+)(\d+)$/);
124
+ if (!match) {
125
+ throw new UserError(`Invalid A1 cell reference: ${a1}`);
126
+ }
127
+ }
128
+ function unquoteSheetName(sheetName) {
129
+ const trimmed = sheetName.trim();
130
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
131
+ return trimmed.slice(1, -1).replace(/''/g, "'");
132
+ }
133
+ return trimmed;
134
+ }
135
+ function quoteSheetName(sheetName) {
136
+ return `'${sheetName.replace(/'/g, "''")}'`;
137
+ }
138
+ async function resolveSheetId(sheets, spreadsheetId, sheetName) {
139
+ const response = await sheets.spreadsheets.get({
140
+ spreadsheetId,
141
+ fields: 'sheets.properties(sheetId,title)',
142
+ });
143
+ for (const sheet of response.data.sheets || []) {
144
+ const props = sheet.properties;
145
+ if (props?.title === sheetName && props.sheetId != null) {
146
+ return props.sheetId;
147
+ }
148
+ }
149
+ throw new UserError(`Sheet "${sheetName}" was not found in spreadsheet ${spreadsheetId}.`);
150
+ }
151
+ async function readRangeText(sheets, spreadsheetId, range) {
152
+ const response = await sheets.spreadsheets.values.get({
153
+ spreadsheetId,
154
+ range,
155
+ valueRenderOption: 'FORMATTED_VALUE',
156
+ });
157
+ const values = response.data.values || [];
158
+ if (values.length === 0) {
159
+ return undefined;
160
+ }
161
+ const lines = values
162
+ .map((row) => row
163
+ .map((cell) => String(cell))
164
+ .join('\t')
165
+ .trimEnd())
166
+ .filter((line) => line.length > 0);
167
+ return lines.length > 0 ? lines.join('\n') : undefined;
168
+ }
@@ -0,0 +1,32 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { google } from 'googleapis';
4
+ import { getAuthClient } from '../../../clients.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'deleteSheetsComment',
8
+ description: 'Permanently deletes a comment and all its replies from a Google Spreadsheet.',
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
+ commentId: z.string().describe('The ID of the comment to delete.'),
14
+ }),
15
+ execute: async (args, { log }) => {
16
+ log.info(`Deleting comment ${args.commentId} from spreadsheet ${args.spreadsheetId}`);
17
+ try {
18
+ const authClient = await getAuthClient();
19
+ const drive = google.drive({ version: 'v3', auth: authClient });
20
+ await drive.comments.delete({
21
+ fileId: args.spreadsheetId,
22
+ commentId: args.commentId,
23
+ });
24
+ return `Comment ${args.commentId} has been deleted.`;
25
+ }
26
+ catch (error) {
27
+ log.error(`Error deleting sheets comment: ${error.message || error}`);
28
+ throw new UserError(`Failed to delete comment: ${error.message || 'Unknown error'}`);
29
+ }
30
+ },
31
+ });
32
+ }
@@ -0,0 +1,51 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { google } from 'googleapis';
4
+ import { getAuthClient } from '../../../clients.js';
5
+ import { extractCommentLocation, rowColToA1 } from './commentAnchor.js';
6
+ export function register(server) {
7
+ server.addTool({
8
+ name: 'getSheetsComment',
9
+ description: 'Gets a specific comment and its full reply thread from a Google Spreadsheet. Use listSheetsComments first to find the comment ID.',
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
+ commentId: z.string().describe('The ID of the comment to retrieve.'),
15
+ }),
16
+ execute: async (args, { log }) => {
17
+ log.info(`Getting comment ${args.commentId} from spreadsheet ${args.spreadsheetId}`);
18
+ try {
19
+ const authClient = await getAuthClient();
20
+ const drive = google.drive({ version: 'v3', auth: authClient });
21
+ const response = await drive.comments.get({
22
+ fileId: args.spreadsheetId,
23
+ commentId: args.commentId,
24
+ fields: 'id,content,anchor,quotedFileContent,author,createdTime,modifiedTime,resolved,replies(id,content,author,createdTime)',
25
+ });
26
+ const comment = response.data;
27
+ const cellInfo = extractCommentLocation(comment);
28
+ return JSON.stringify({
29
+ id: comment.id,
30
+ author: comment.author?.displayName || null,
31
+ content: comment.content,
32
+ cell: cellInfo ? rowColToA1(cellInfo.row, cellInfo.col) : null,
33
+ quotedText: comment.quotedFileContent?.value || null,
34
+ resolved: comment.resolved || false,
35
+ createdTime: comment.createdTime,
36
+ modifiedTime: comment.modifiedTime,
37
+ replies: (comment.replies || []).map((r) => ({
38
+ id: r.id,
39
+ author: r.author?.displayName || null,
40
+ content: r.content,
41
+ createdTime: r.createdTime,
42
+ })),
43
+ }, null, 2);
44
+ }
45
+ catch (error) {
46
+ log.error(`Error getting sheets comment: ${error.message || error}`);
47
+ throw new UserError(`Failed to get comment: ${error.message || 'Unknown error'}`);
48
+ }
49
+ },
50
+ });
51
+ }
@@ -0,0 +1,16 @@
1
+ import { register as createSheetsComment } from './createSheetsComment.js';
2
+ import { register as createSheetsCellNote } from './createSheetsCellNote.js';
3
+ import { register as listSheetsComments } from './listSheetsComments.js';
4
+ import { register as getSheetsComment } from './getSheetsComment.js';
5
+ import { register as replyToSheetsComment } from './replyToSheetsComment.js';
6
+ import { register as resolveSheetsComment } from './resolveSheetsComment.js';
7
+ import { register as deleteSheetsComment } from './deleteSheetsComment.js';
8
+ export function registerSheetsCommentTools(server) {
9
+ createSheetsComment(server);
10
+ createSheetsCellNote(server);
11
+ listSheetsComments(server);
12
+ getSheetsComment(server);
13
+ replyToSheetsComment(server);
14
+ resolveSheetsComment(server);
15
+ deleteSheetsComment(server);
16
+ }
@@ -0,0 +1,177 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { google } from 'googleapis';
4
+ import { getAuthClient, getSheetsClient } from '../../../clients.js';
5
+ import { extractCommentLocation, a1ToRowCol, rowColToA1 } from './commentAnchor.js';
6
+ export function register(server) {
7
+ server.addTool({
8
+ name: 'listSheetsComments',
9
+ description: 'Lists all comments in a Google Spreadsheet. Filter by sheet name, specific row(s), specific cell, or a cell range. Returns comment IDs needed for getSheetsComment, replyToSheetsComment, resolveSheetsComment, or deleteSheetsComment.',
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
+ sheetName: z
15
+ .string()
16
+ .optional()
17
+ .describe('Optional: filter comments to only this sheet/tab name.'),
18
+ row: z
19
+ .number()
20
+ .int()
21
+ .min(1)
22
+ .optional()
23
+ .describe('Optional: filter to comments on this specific row number (1-based, e.g., 5 for row 5).'),
24
+ rows: z
25
+ .array(z.number().int().min(1))
26
+ .optional()
27
+ .describe('Optional: filter to comments on any of these row numbers (1-based, e.g., [3, 5, 12]). Ignored if "row" is set.'),
28
+ cell: z
29
+ .string()
30
+ .optional()
31
+ .describe('Optional: filter to comments on this specific cell in A1 notation (e.g., "B3"). Overrides row/rows filters.'),
32
+ range: z
33
+ .string()
34
+ .optional()
35
+ .describe('Optional: filter to comments within this A1 range (e.g., "A1:D10", "B:B" for entire column B). Overrides row/rows/cell filters.'),
36
+ includeResolved: z
37
+ .boolean()
38
+ .optional()
39
+ .default(false)
40
+ .describe('If true, include resolved comments. Defaults to false.'),
41
+ }),
42
+ execute: async (args, { log }) => {
43
+ log.info(`Listing comments for spreadsheet ${args.spreadsheetId}`);
44
+ try {
45
+ const authClient = await getAuthClient();
46
+ const drive = google.drive({ version: 'v3', auth: authClient });
47
+ const comments = [];
48
+ let pageToken;
49
+ do {
50
+ const response = await drive.comments.list({
51
+ fileId: args.spreadsheetId,
52
+ fields: 'comments(id,content,anchor,quotedFileContent,author,createdTime,modifiedTime,resolved,replies(id,content,author,createdTime)),nextPageToken',
53
+ pageSize: 100,
54
+ ...(pageToken ? { pageToken } : {}),
55
+ });
56
+ comments.push(...(response.data.comments || []));
57
+ pageToken = response.data.nextPageToken || undefined;
58
+ } while (pageToken);
59
+ let sheetGidMap = {};
60
+ const needSheetMeta = args.sheetName || args.row || args.rows || args.cell || args.range;
61
+ if (needSheetMeta) {
62
+ const sheetsClient = await getSheetsClient();
63
+ const meta = await sheetsClient.spreadsheets.get({
64
+ spreadsheetId: args.spreadsheetId,
65
+ fields: 'sheets.properties',
66
+ });
67
+ for (const sheet of meta.data.sheets || []) {
68
+ const props = sheet.properties;
69
+ if (props?.sheetId !== undefined && props?.title) {
70
+ sheetGidMap[String(props.sheetId)] = props.title;
71
+ }
72
+ }
73
+ }
74
+ const rangeFilter = args.range ? parseA1Range(args.range) : null;
75
+ const cellFilter = args.cell ? a1ToRowCol(args.cell) : null;
76
+ const rowSet = args.row
77
+ ? new Set([args.row - 1])
78
+ : args.rows
79
+ ? new Set(args.rows.map((r) => r - 1))
80
+ : null;
81
+ const result = comments
82
+ .filter((c) => {
83
+ if (!args.includeResolved && c.resolved)
84
+ return false;
85
+ const cellInfo = extractCommentLocation(c);
86
+ if (args.sheetName && cellInfo) {
87
+ const resolvedName = sheetGidMap[String(cellInfo.sheetId)];
88
+ if (resolvedName && resolvedName !== args.sheetName)
89
+ return false;
90
+ }
91
+ if (!cellInfo)
92
+ return !rangeFilter && !cellFilter && !rowSet;
93
+ if (rangeFilter) {
94
+ return (cellInfo.row >= rangeFilter.startRow &&
95
+ cellInfo.row <= rangeFilter.endRow &&
96
+ cellInfo.col >= rangeFilter.startCol &&
97
+ cellInfo.col <= rangeFilter.endCol);
98
+ }
99
+ if (cellFilter) {
100
+ return cellInfo.row === cellFilter.row && cellInfo.col === cellFilter.col;
101
+ }
102
+ // Guard: a malformed cell arg yields a null cellFilter; fall through
103
+ // means no cell constraint applied, which matches prior behavior.
104
+ if (rowSet) {
105
+ return rowSet.has(cellInfo.row);
106
+ }
107
+ return true;
108
+ })
109
+ .map((comment) => {
110
+ const cellInfo = extractCommentLocation(comment);
111
+ let cellRef = null;
112
+ let sheetTitle = null;
113
+ if (cellInfo) {
114
+ cellRef = rowColToA1(cellInfo.row, cellInfo.col);
115
+ if (Object.keys(sheetGidMap).length > 0) {
116
+ sheetTitle = sheetGidMap[String(cellInfo.sheetId)] || null;
117
+ }
118
+ }
119
+ return {
120
+ id: comment.id,
121
+ author: comment.author?.displayName || null,
122
+ content: comment.content,
123
+ cell: cellRef,
124
+ sheetName: sheetTitle,
125
+ quotedText: comment.quotedFileContent?.value || null,
126
+ resolved: comment.resolved || false,
127
+ createdTime: comment.createdTime,
128
+ modifiedTime: comment.modifiedTime,
129
+ replyCount: comment.replies?.length || 0,
130
+ };
131
+ });
132
+ return JSON.stringify({ comments: result, total: result.length }, null, 2);
133
+ }
134
+ catch (error) {
135
+ log.error(`Error listing sheets comments: ${error.message || error}`);
136
+ throw new UserError(`Failed to list spreadsheet comments: ${error.message || 'Unknown error'}`);
137
+ }
138
+ },
139
+ });
140
+ }
141
+ function parseA1Range(range) {
142
+ const stripped = range.includes('!') ? range.split('!')[1] : range;
143
+ const parts = stripped.split(':');
144
+ const colOnly = /^[A-Za-z]+$/;
145
+ const rowOnly = /^\d+$/;
146
+ const parseCorner = (s) => {
147
+ if (colOnly.test(s)) {
148
+ const upper = s.toUpperCase();
149
+ let c = 0;
150
+ for (let i = 0; i < upper.length; i++)
151
+ c = c * 26 + (upper.charCodeAt(i) - 64);
152
+ return { row: null, col: c - 1 };
153
+ }
154
+ if (rowOnly.test(s)) {
155
+ return { row: parseInt(s, 10) - 1, col: null };
156
+ }
157
+ const rc = a1ToRowCol(s);
158
+ return rc ? { row: rc.row, col: rc.col } : { row: null, col: null };
159
+ };
160
+ if (parts.length === 1) {
161
+ const p = parseCorner(parts[0]);
162
+ return {
163
+ startRow: p.row ?? 0,
164
+ endRow: p.row ?? 999999,
165
+ startCol: p.col ?? 0,
166
+ endCol: p.col ?? 999999,
167
+ };
168
+ }
169
+ const start = parseCorner(parts[0]);
170
+ const end = parseCorner(parts[1]);
171
+ return {
172
+ startRow: start.row ?? 0,
173
+ endRow: end.row ?? 999999,
174
+ startCol: start.col ?? 0,
175
+ endCol: end.col ?? 999999,
176
+ };
177
+ }
@@ -0,0 +1,37 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { google } from 'googleapis';
4
+ import { getAuthClient } from '../../../clients.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'replyToSheetsComment',
8
+ description: 'Adds a reply to an existing comment thread in a Google Spreadsheet. Use listSheetsComments or getSheetsComment to find the comment 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
+ commentId: z.string().describe('The ID of the comment to reply to.'),
14
+ content: z.string().min(1).describe('The text content of the reply.'),
15
+ }),
16
+ execute: async (args, { log }) => {
17
+ log.info(`Adding reply to comment ${args.commentId} in spreadsheet ${args.spreadsheetId}`);
18
+ try {
19
+ const authClient = await getAuthClient();
20
+ const drive = google.drive({ version: 'v3', auth: authClient });
21
+ const response = await drive.replies.create({
22
+ fileId: args.spreadsheetId,
23
+ commentId: args.commentId,
24
+ fields: 'id,content,author,createdTime',
25
+ requestBody: {
26
+ content: args.content,
27
+ },
28
+ });
29
+ return `Reply added successfully. Reply ID: ${response.data.id}`;
30
+ }
31
+ catch (error) {
32
+ log.error(`Error adding reply to sheets comment: ${error.message || error}`);
33
+ throw new UserError(`Failed to add reply: ${error.message || 'Unknown error'}`);
34
+ }
35
+ },
36
+ });
37
+ }
@@ -0,0 +1,53 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { google } from 'googleapis';
4
+ import { getAuthClient } from '../../../clients.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'resolveSheetsComment',
8
+ description: 'Marks a comment as resolved in a Google Spreadsheet. Note: resolved status may not persist in the Sheets UI due to a Drive API limitation.',
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
+ commentId: z.string().describe('The ID of the comment to resolve.'),
14
+ }),
15
+ execute: async (args, { log }) => {
16
+ log.info(`Resolving comment ${args.commentId} in spreadsheet ${args.spreadsheetId}`);
17
+ try {
18
+ const authClient = await getAuthClient();
19
+ const drive = google.drive({ version: 'v3', auth: authClient });
20
+ const currentComment = await drive.comments.get({
21
+ fileId: args.spreadsheetId,
22
+ commentId: args.commentId,
23
+ fields: 'content',
24
+ });
25
+ await drive.comments.update({
26
+ fileId: args.spreadsheetId,
27
+ commentId: args.commentId,
28
+ fields: 'id,resolved',
29
+ requestBody: {
30
+ content: currentComment.data.content,
31
+ resolved: true,
32
+ },
33
+ });
34
+ const verifyComment = await drive.comments.get({
35
+ fileId: args.spreadsheetId,
36
+ commentId: args.commentId,
37
+ fields: 'resolved',
38
+ });
39
+ if (verifyComment.data.resolved) {
40
+ return `Comment ${args.commentId} has been marked as resolved.`;
41
+ }
42
+ else {
43
+ return `Attempted to resolve comment ${args.commentId}, but the resolved status may not persist in the Sheets UI due to API limitations. The comment can be resolved manually in the Google Sheets interface.`;
44
+ }
45
+ }
46
+ catch (error) {
47
+ log.error(`Error resolving sheets comment: ${error.message || error}`);
48
+ const errorDetails = error.response?.data?.error?.message || error.message || 'Unknown error';
49
+ throw new UserError(`Failed to resolve comment: ${errorDetails}`);
50
+ }
51
+ },
52
+ });
53
+ }
@@ -0,0 +1,59 @@
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: 'copyFormatting',
8
+ description: 'Copies formatting (not values) from a source range to a destination range within the same spreadsheet. Copies bold, colors, borders, number formats, etc.',
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
+ sourceSheetName: z.string().min(1).describe('Source sheet/tab name (e.g., "Sheet1").'),
14
+ sourceRange: z
15
+ .string()
16
+ .describe('A1 notation for the source range (e.g., "A1:D10"). Do not include the sheet name — use sourceSheetName instead.'),
17
+ destinationSheetName: z
18
+ .string()
19
+ .min(1)
20
+ .describe('Destination sheet/tab name (e.g., "Sheet2").'),
21
+ destinationRange: z
22
+ .string()
23
+ .describe('A1 notation for the destination range (e.g., "A1:D10"). Do not include the sheet name — use destinationSheetName instead.'),
24
+ }),
25
+ execute: async (args, { log }) => {
26
+ const sheets = await getSheetsClient();
27
+ log.info(`Copying formatting from ${args.sourceSheetName}!${args.sourceRange} to ${args.destinationSheetName}!${args.destinationRange} in spreadsheet ${args.spreadsheetId}`);
28
+ try {
29
+ // Resolve sheet names to numeric IDs
30
+ const sourceSheetId = await SheetsHelpers.resolveSheetId(sheets, args.spreadsheetId, args.sourceSheetName);
31
+ const destSheetId = await SheetsHelpers.resolveSheetId(sheets, args.spreadsheetId, args.destinationSheetName);
32
+ // Convert A1 ranges to GridRange objects
33
+ const sourceGridRange = SheetsHelpers.parseA1ToGridRange(args.sourceRange, sourceSheetId);
34
+ const destGridRange = SheetsHelpers.parseA1ToGridRange(args.destinationRange, destSheetId);
35
+ await sheets.spreadsheets.batchUpdate({
36
+ spreadsheetId: args.spreadsheetId,
37
+ requestBody: {
38
+ requests: [
39
+ {
40
+ copyPaste: {
41
+ source: sourceGridRange,
42
+ destination: destGridRange,
43
+ pasteType: 'PASTE_FORMAT',
44
+ },
45
+ },
46
+ ],
47
+ },
48
+ });
49
+ return `Successfully copied formatting from ${args.sourceSheetName}!${args.sourceRange} to ${args.destinationSheetName}!${args.destinationRange}.`;
50
+ }
51
+ catch (error) {
52
+ log.error(`Error copying formatting in spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
53
+ if (error instanceof UserError)
54
+ throw error;
55
+ throw new UserError(`Failed to copy formatting: ${error.message || 'Unknown error'}`);
56
+ }
57
+ },
58
+ });
59
+ }
@@ -0,0 +1,36 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSheetsClient } from '../../clients.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'copySheetTo',
7
+ description: 'Copies a sheet (tab) from one spreadsheet to another spreadsheet. Use getSpreadsheetInfo to find the numeric sheet ID. The copied sheet will be appended to the destination spreadsheet.',
8
+ parameters: z.strictObject({
9
+ sourceSpreadsheetId: z.string().describe('The spreadsheet ID of the source file.'),
10
+ sheetId: z
11
+ .number()
12
+ .int()
13
+ .describe('The numeric sheet ID to copy. Use getSpreadsheetInfo to find sheet IDs.'),
14
+ destinationSpreadsheetId: z.string().describe('The spreadsheet ID of the destination file.'),
15
+ }),
16
+ execute: async (args, { log }) => {
17
+ const sheets = await getSheetsClient();
18
+ log.info(`Copying sheet ${args.sheetId} from ${args.sourceSpreadsheetId} to ${args.destinationSpreadsheetId}`);
19
+ try {
20
+ const response = await sheets.spreadsheets.sheets.copyTo({
21
+ spreadsheetId: args.sourceSpreadsheetId,
22
+ sheetId: args.sheetId,
23
+ requestBody: { destinationSpreadsheetId: args.destinationSpreadsheetId },
24
+ });
25
+ const props = response.data;
26
+ return `Successfully copied sheet to destination as "${props.title}" (Sheet ID: ${props.sheetId}).`;
27
+ }
28
+ catch (error) {
29
+ log.error(`Error copying sheet: ${error.message || error}`);
30
+ if (error instanceof UserError)
31
+ throw error;
32
+ throw new UserError(`Failed to copy sheet: ${error.message || 'Unknown error'}`);
33
+ }
34
+ },
35
+ });
36
+ }