mcp-google-extras 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +80 -0
  3. package/dist/auth.js +225 -0
  4. package/dist/cachedToolsList.js +38 -0
  5. package/dist/clients.js +92 -0
  6. package/dist/googleDocsApiHelpers.js +883 -0
  7. package/dist/googleSheetsApiHelpers.js +808 -0
  8. package/dist/index.js +54 -0
  9. package/dist/logger.js +58 -0
  10. package/dist/markdown-transformer/docsToMarkdown.js +259 -0
  11. package/dist/markdown-transformer/index.js +126 -0
  12. package/dist/markdown-transformer/markdownToDocs.js +834 -0
  13. package/dist/tools/docs/addTab.js +92 -0
  14. package/dist/tools/docs/appendToGoogleDoc.js +81 -0
  15. package/dist/tools/docs/comments/addComment.js +83 -0
  16. package/dist/tools/docs/comments/deleteComment.js +30 -0
  17. package/dist/tools/docs/comments/getComment.js +45 -0
  18. package/dist/tools/docs/comments/index.js +14 -0
  19. package/dist/tools/docs/comments/listComments.js +43 -0
  20. package/dist/tools/docs/comments/replyToComment.js +35 -0
  21. package/dist/tools/docs/comments/resolveComment.js +55 -0
  22. package/dist/tools/docs/deleteRange.js +72 -0
  23. package/dist/tools/docs/findAndReplace.js +54 -0
  24. package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
  25. package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
  26. package/dist/tools/docs/formatting/index.js +6 -0
  27. package/dist/tools/docs/index.js +38 -0
  28. package/dist/tools/docs/insertImage.js +122 -0
  29. package/dist/tools/docs/insertPageBreak.js +58 -0
  30. package/dist/tools/docs/insertTable.js +53 -0
  31. package/dist/tools/docs/insertTableWithData.js +135 -0
  32. package/dist/tools/docs/insertText.js +61 -0
  33. package/dist/tools/docs/listDocumentTabs.js +60 -0
  34. package/dist/tools/docs/modifyText.js +158 -0
  35. package/dist/tools/docs/readGoogleDoc.js +165 -0
  36. package/dist/tools/docs/renameTab.js +61 -0
  37. package/dist/tools/drive/copyFile.js +63 -0
  38. package/dist/tools/drive/createDocument.js +89 -0
  39. package/dist/tools/drive/createFolder.js +48 -0
  40. package/dist/tools/drive/createFromTemplate.js +82 -0
  41. package/dist/tools/drive/deleteFile.js +72 -0
  42. package/dist/tools/drive/getDocumentInfo.js +48 -0
  43. package/dist/tools/drive/getFolderInfo.js +48 -0
  44. package/dist/tools/drive/index.js +26 -0
  45. package/dist/tools/drive/listFolderContents.js +82 -0
  46. package/dist/tools/drive/listGoogleDocs.js +67 -0
  47. package/dist/tools/drive/moveFile.js +54 -0
  48. package/dist/tools/drive/renameFile.js +39 -0
  49. package/dist/tools/drive/searchGoogleDocs.js +73 -0
  50. package/dist/tools/extras/index.js +7 -0
  51. package/dist/tools/extras/readFile.js +82 -0
  52. package/dist/tools/extras/searchFileContents.js +81 -0
  53. package/dist/tools/index.js +15 -0
  54. package/dist/tools/sheets/addConditionalFormatting.js +143 -0
  55. package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
  56. package/dist/tools/sheets/appendSpreadsheetRows.js +43 -0
  57. package/dist/tools/sheets/appendTableRows.js +50 -0
  58. package/dist/tools/sheets/autoResizeColumns.js +67 -0
  59. package/dist/tools/sheets/batchWrite.js +59 -0
  60. package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
  61. package/dist/tools/sheets/copyFormatting.js +59 -0
  62. package/dist/tools/sheets/createSpreadsheet.js +71 -0
  63. package/dist/tools/sheets/createTable.js +120 -0
  64. package/dist/tools/sheets/deleteChart.js +41 -0
  65. package/dist/tools/sheets/deleteSheet.js +43 -0
  66. package/dist/tools/sheets/deleteTable.js +56 -0
  67. package/dist/tools/sheets/duplicateSheet.js +53 -0
  68. package/dist/tools/sheets/formatCells.js +106 -0
  69. package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
  70. package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
  71. package/dist/tools/sheets/getTable.js +48 -0
  72. package/dist/tools/sheets/groupRows.js +62 -0
  73. package/dist/tools/sheets/index.js +66 -0
  74. package/dist/tools/sheets/insertChart.js +225 -0
  75. package/dist/tools/sheets/listGoogleSheets.js +62 -0
  76. package/dist/tools/sheets/listTables.js +55 -0
  77. package/dist/tools/sheets/readCellFormat.js +143 -0
  78. package/dist/tools/sheets/readSpreadsheet.js +36 -0
  79. package/dist/tools/sheets/renameSheet.js +48 -0
  80. package/dist/tools/sheets/setColumnWidths.js +43 -0
  81. package/dist/tools/sheets/setDropdownValidation.js +51 -0
  82. package/dist/tools/sheets/ungroupAllRows.js +66 -0
  83. package/dist/tools/sheets/updateTableRange.js +51 -0
  84. package/dist/tools/sheets/writeSpreadsheet.js +43 -0
  85. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +93 -0
  86. package/dist/tools/utils/index.js +6 -0
  87. package/dist/tools/utils/replaceDocumentWithMarkdown.js +154 -0
  88. package/dist/types.js +186 -0
  89. package/package.json +47 -0
@@ -0,0 +1,48 @@
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: 'getTable',
8
+ description: 'Gets detailed information about a specific table including its columns, range, and properties. Use the table name or ID returned by listTables.',
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
+ tableIdentifier: z
14
+ .string()
15
+ .describe('The table name or table ID. Names are resolved first, then IDs. Use listTables to see available tables.'),
16
+ }),
17
+ execute: async (args, { log }) => {
18
+ const sheets = await getSheetsClient();
19
+ log.info(`Getting table details for: ${args.tableIdentifier}`);
20
+ try {
21
+ const { table, sheetName, sheetId } = await SheetsHelpers.resolveTableIdentifier(sheets, args.spreadsheetId, args.tableIdentifier);
22
+ // Build detailed table information
23
+ const columns = table.columnProperties?.map((col) => ({
24
+ index: col.columnIndex,
25
+ name: col.columnName,
26
+ })) || [];
27
+ const range = table.range
28
+ ? `${sheetName}!${SheetsHelpers.rowColToA1(table.range.startRowIndex || 0, table.range.startColumnIndex || 0)}:${SheetsHelpers.rowColToA1((table.range.endRowIndex || 1) - 1, (table.range.endColumnIndex || 1) - 1)}`
29
+ : 'Unknown';
30
+ return JSON.stringify({
31
+ tableId: table.tableId,
32
+ name: table.name,
33
+ sheetName,
34
+ sheetId,
35
+ range,
36
+ columns,
37
+ columnCount: table.columnProperties?.length || 0,
38
+ }, null, 2);
39
+ }
40
+ catch (error) {
41
+ log.error(`Error getting table details: ${error.message || error}`);
42
+ if (error instanceof UserError)
43
+ throw error;
44
+ throw new UserError(`Failed to get table details: ${error.message || 'Unknown error'}`);
45
+ }
46
+ },
47
+ });
48
+ }
@@ -0,0 +1,62 @@
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: 'groupRows',
8
+ description: 'Creates collapsible row groups in a Google Sheet using the Sheets API addDimensionGroup request. Each group specifies a range of rows (1-based, inclusive) to collapse.',
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
+ sheetName: z
14
+ .string()
15
+ .optional()
16
+ .describe('Name of the sheet/tab. Defaults to the first sheet if not provided.'),
17
+ groups: z
18
+ .array(z.object({
19
+ startRowIndex: z
20
+ .number()
21
+ .int()
22
+ .describe('1-based row number of the first row in the group (inclusive).'),
23
+ endRowIndex: z
24
+ .number()
25
+ .int()
26
+ .describe('1-based row number of the last row in the group (inclusive).'),
27
+ }))
28
+ .min(1)
29
+ .describe('Array of row ranges to group. Each entry is {startRowIndex, endRowIndex} using 1-based row numbers.'),
30
+ }),
31
+ execute: async (args, { log }) => {
32
+ const sheets = await getSheetsClient();
33
+ log.info(`Grouping rows in spreadsheet ${args.spreadsheetId}`);
34
+ try {
35
+ const sheetId = await SheetsHelpers.resolveSheetId(sheets, args.spreadsheetId, args.sheetName);
36
+ // Convert 1-based inclusive row numbers to 0-based exclusive indices
37
+ // required by the Sheets API DimensionRange
38
+ const requests = args.groups.map(({ startRowIndex, endRowIndex }) => ({
39
+ addDimensionGroup: {
40
+ range: {
41
+ sheetId,
42
+ dimension: 'ROWS',
43
+ startIndex: startRowIndex - 1, // 0-based, inclusive
44
+ endIndex: endRowIndex, // 0-based, exclusive (= 1-based inclusive)
45
+ },
46
+ },
47
+ }));
48
+ await sheets.spreadsheets.batchUpdate({
49
+ spreadsheetId: args.spreadsheetId,
50
+ requestBody: { requests },
51
+ });
52
+ return `Successfully created ${args.groups.length} row group(s).`;
53
+ }
54
+ catch (error) {
55
+ log.error(`Error grouping rows: ${error.message || error}`);
56
+ if (error instanceof UserError)
57
+ throw error;
58
+ throw new UserError(`Failed to group rows: ${error.message || 'Unknown error'}`);
59
+ }
60
+ },
61
+ });
62
+ }
@@ -0,0 +1,66 @@
1
+ import { register as readSpreadsheet } from './readSpreadsheet.js';
2
+ import { register as writeSpreadsheet } from './writeSpreadsheet.js';
3
+ import { register as batchWrite } from './batchWrite.js';
4
+ import { register as appendSpreadsheetRows } from './appendSpreadsheetRows.js';
5
+ import { register as clearSpreadsheetRange } from './clearSpreadsheetRange.js';
6
+ import { register as getSpreadsheetInfo } from './getSpreadsheetInfo.js';
7
+ import { register as addSpreadsheetSheet } from './addSpreadsheetSheet.js';
8
+ import { register as createSpreadsheet } from './createSpreadsheet.js';
9
+ import { register as listGoogleSheets } from './listGoogleSheets.js';
10
+ import { register as deleteSheet } from './deleteSheet.js';
11
+ import { register as renameSheet } from './renameSheet.js';
12
+ import { register as duplicateSheet } from './duplicateSheet.js';
13
+ // Formatting & validation
14
+ import { register as formatCells } from './formatCells.js';
15
+ import { register as readCellFormat } from './readCellFormat.js';
16
+ import { register as copyFormatting } from './copyFormatting.js';
17
+ import { register as freezeRowsAndColumns } from './freezeRowsAndColumns.js';
18
+ import { register as setColumnWidths } from './setColumnWidths.js';
19
+ import { register as autoResizeColumns } from './autoResizeColumns.js';
20
+ import { register as setDropdownValidation } from './setDropdownValidation.js';
21
+ import { register as addConditionalFormatting } from './addConditionalFormatting.js';
22
+ import { register as groupRows } from './groupRows.js';
23
+ import { register as ungroupAllRows } from './ungroupAllRows.js';
24
+ // Tables
25
+ import { register as createTable } from './createTable.js';
26
+ import { register as listTables } from './listTables.js';
27
+ import { register as getTable } from './getTable.js';
28
+ import { register as deleteTable } from './deleteTable.js';
29
+ import { register as updateTableRange } from './updateTableRange.js';
30
+ import { register as appendTableRows } from './appendTableRows.js';
31
+ import { register as insertChart } from './insertChart.js';
32
+ import { register as deleteChart } from './deleteChart.js';
33
+ export function registerSheetsTools(server) {
34
+ readSpreadsheet(server);
35
+ writeSpreadsheet(server);
36
+ batchWrite(server);
37
+ appendSpreadsheetRows(server);
38
+ clearSpreadsheetRange(server);
39
+ getSpreadsheetInfo(server);
40
+ addSpreadsheetSheet(server);
41
+ createSpreadsheet(server);
42
+ listGoogleSheets(server);
43
+ deleteSheet(server);
44
+ renameSheet(server);
45
+ duplicateSheet(server);
46
+ // Formatting & validation
47
+ formatCells(server);
48
+ readCellFormat(server);
49
+ copyFormatting(server);
50
+ freezeRowsAndColumns(server);
51
+ setColumnWidths(server);
52
+ autoResizeColumns(server);
53
+ setDropdownValidation(server);
54
+ addConditionalFormatting(server);
55
+ groupRows(server);
56
+ ungroupAllRows(server);
57
+ // Tables
58
+ createTable(server);
59
+ listTables(server);
60
+ getTable(server);
61
+ deleteTable(server);
62
+ updateTableRange(server);
63
+ appendTableRows(server);
64
+ insertChart(server);
65
+ deleteChart(server);
66
+ }
@@ -0,0 +1,225 @@
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: 'insertChart',
8
+ description: 'Inserts a chart into a Google Sheet. Supports bar, column, line, area, scatter, pie, donut, and treemap (hierarchical) chart types. ' +
9
+ 'For treemap charts, the data must have a label column and a parent label column (use empty string for root nodes) plus a numeric size column. ' +
10
+ 'Chart is placed as an overlay at the specified anchor cell.',
11
+ parameters: z.object({
12
+ spreadsheetId: z
13
+ .string()
14
+ .describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
15
+ sheetName: z
16
+ .string()
17
+ .optional()
18
+ .describe('Name of the sheet/tab containing the data. Defaults to the first sheet.'),
19
+ chartType: z
20
+ .enum(['BAR', 'COLUMN', 'LINE', 'AREA', 'SCATTER', 'PIE', 'DONUT', 'TREEMAP'])
21
+ .describe('Chart type to create.'),
22
+ stackedType: z
23
+ .enum(['NOT_STACKED', 'STACKED', 'PERCENT_STACKED'])
24
+ .default('NOT_STACKED')
25
+ .describe('For bar/column/area charts: whether to stack series. NOT_STACKED = grouped, STACKED = absolute stacked, PERCENT_STACKED = 100% stacked.'),
26
+ title: z.string().optional().describe('Chart title.'),
27
+ dataRange: z
28
+ .string()
29
+ .describe('A1 notation range of the data (e.g., "A1:E50"). Include the header row if present.'),
30
+ headerRow: z
31
+ .boolean()
32
+ .default(true)
33
+ .describe('Whether the first row of the data range is a header row.'),
34
+ // Column indices for flexible data mapping
35
+ labelColumnIndex: z
36
+ .number()
37
+ .int()
38
+ .min(1)
39
+ .optional()
40
+ .describe('For pie/donut/treemap: 1-based column index for node labels (default: 1). For treemap this is the leaf/child label.'),
41
+ parentColumnIndex: z
42
+ .number()
43
+ .int()
44
+ .min(1)
45
+ .optional()
46
+ .describe('For treemap: 1-based column index for parent node labels. Root nodes should have an empty string in this column.'),
47
+ valueColumnIndex: z
48
+ .number()
49
+ .int()
50
+ .min(1)
51
+ .optional()
52
+ .describe('For pie/donut/treemap: 1-based column index for the numeric size/value (default: 2).'),
53
+ // Chart position and size
54
+ anchorRow: z
55
+ .number()
56
+ .int()
57
+ .min(0)
58
+ .default(0)
59
+ .describe('Row index (0-based) of the anchor cell for chart placement.'),
60
+ anchorColumn: z
61
+ .number()
62
+ .int()
63
+ .min(0)
64
+ .default(6)
65
+ .describe('Column index (0-based) of the anchor cell for chart placement.'),
66
+ offsetXPixels: z
67
+ .number()
68
+ .int()
69
+ .default(0)
70
+ .describe('Horizontal offset in pixels from the anchor cell.'),
71
+ offsetYPixels: z
72
+ .number()
73
+ .int()
74
+ .default(0)
75
+ .describe('Vertical offset in pixels from the anchor cell.'),
76
+ widthPixels: z.number().int().default(600).describe('Chart width in pixels.'),
77
+ heightPixels: z.number().int().default(400).describe('Chart height in pixels.'),
78
+ }),
79
+ execute: async (args, { log }) => {
80
+ const sheets = await getSheetsClient();
81
+ log.info(`Inserting ${args.chartType} chart into spreadsheet ${args.spreadsheetId}`);
82
+ try {
83
+ const sheetId = await SheetsHelpers.resolveSheetId(sheets, args.spreadsheetId, args.sheetName);
84
+ const { a1Range } = SheetsHelpers.parseRange(args.dataRange);
85
+ const gridRange = SheetsHelpers.parseA1ToGridRange(a1Range, sheetId);
86
+ const startRow = gridRange.startRowIndex ?? 0;
87
+ const endRow = gridRange.endRowIndex ?? startRow + 1;
88
+ const startCol = gridRange.startColumnIndex ?? 0;
89
+ const endCol = gridRange.endColumnIndex ?? startCol + 1;
90
+ const dataStartRow = args.headerRow ? startRow + 1 : startRow;
91
+ const labelCol = startCol + (args.labelColumnIndex ? args.labelColumnIndex - 1 : 0);
92
+ const valueCol = startCol + (args.valueColumnIndex ? args.valueColumnIndex - 1 : 1);
93
+ const parentCol = startCol + (args.parentColumnIndex ? args.parentColumnIndex - 1 : 0);
94
+ const makeSourceRange = (colStart, colEnd, rowStart = dataStartRow) => ({
95
+ sources: [
96
+ {
97
+ sheetId,
98
+ startRowIndex: rowStart,
99
+ endRowIndex: endRow,
100
+ startColumnIndex: colStart,
101
+ endColumnIndex: colEnd,
102
+ },
103
+ ],
104
+ });
105
+ let chartSpec = {};
106
+ if (args.chartType === 'PIE' || args.chartType === 'DONUT') {
107
+ chartSpec.pieChart = {
108
+ legendPosition: 'LABELED_LEGEND',
109
+ pieHole: args.chartType === 'DONUT' ? 0.5 : 0,
110
+ domain: {
111
+ data: { sourceRange: makeSourceRange(labelCol, labelCol + 1) },
112
+ },
113
+ series: {
114
+ data: { sourceRange: makeSourceRange(valueCol, valueCol + 1) },
115
+ },
116
+ };
117
+ }
118
+ else if (args.chartType === 'TREEMAP') {
119
+ chartSpec.treemapChart = {
120
+ labels: {
121
+ sourceRange: makeSourceRange(labelCol, labelCol + 1),
122
+ },
123
+ parentLabels: {
124
+ sourceRange: makeSourceRange(parentCol, parentCol + 1),
125
+ },
126
+ sizeData: {
127
+ sourceRange: makeSourceRange(valueCol, valueCol + 1),
128
+ },
129
+ colorData: {
130
+ sourceRange: makeSourceRange(valueCol, valueCol + 1),
131
+ },
132
+ };
133
+ }
134
+ else {
135
+ // Basic chart types: BAR, COLUMN, LINE, AREA, SCATTER
136
+ // Each series must be a separate entry with a single-column source range.
137
+ const seriesCount = endCol - startCol - 1;
138
+ const series = Array.from({ length: seriesCount }, (_, i) => ({
139
+ series: {
140
+ sourceRange: {
141
+ sources: [
142
+ {
143
+ sheetId,
144
+ startRowIndex: startRow, // include header so Sheets names the series automatically
145
+ endRowIndex: endRow,
146
+ startColumnIndex: startCol + 1 + i,
147
+ endColumnIndex: startCol + 2 + i,
148
+ },
149
+ ],
150
+ },
151
+ },
152
+ targetAxis: 'LEFT_AXIS',
153
+ }));
154
+ chartSpec.basicChart = {
155
+ chartType: args.chartType,
156
+ stackedType: args.stackedType,
157
+ legendPosition: 'BOTTOM_LEGEND',
158
+ axis: [
159
+ { position: 'BOTTOM_AXIS', title: '' },
160
+ { position: 'LEFT_AXIS', title: '' },
161
+ ],
162
+ domains: [
163
+ {
164
+ domain: {
165
+ sourceRange: {
166
+ sources: [
167
+ {
168
+ sheetId,
169
+ startRowIndex: startRow,
170
+ endRowIndex: endRow,
171
+ startColumnIndex: startCol,
172
+ endColumnIndex: startCol + 1,
173
+ },
174
+ ],
175
+ },
176
+ },
177
+ reversed: false,
178
+ },
179
+ ],
180
+ series,
181
+ headerCount: args.headerRow ? 1 : 0,
182
+ };
183
+ }
184
+ if (args.title) {
185
+ chartSpec.title = args.title;
186
+ }
187
+ const response = await sheets.spreadsheets.batchUpdate({
188
+ spreadsheetId: args.spreadsheetId,
189
+ requestBody: {
190
+ requests: [
191
+ {
192
+ addChart: {
193
+ chart: {
194
+ spec: chartSpec,
195
+ position: {
196
+ overlayPosition: {
197
+ anchorCell: {
198
+ sheetId,
199
+ rowIndex: args.anchorRow,
200
+ columnIndex: args.anchorColumn,
201
+ },
202
+ offsetXPixels: args.offsetXPixels,
203
+ offsetYPixels: args.offsetYPixels,
204
+ widthPixels: args.widthPixels,
205
+ heightPixels: args.heightPixels,
206
+ },
207
+ },
208
+ },
209
+ },
210
+ },
211
+ ],
212
+ },
213
+ });
214
+ const chartId = response.data.replies?.[0]?.addChart?.chart?.chartId;
215
+ return `Chart created successfully${chartId ? ` (Chart ID: ${chartId})` : ''}.`;
216
+ }
217
+ catch (error) {
218
+ log.error(`Error inserting chart: ${error.message || error}`);
219
+ if (error instanceof UserError)
220
+ throw error;
221
+ throw new UserError(`Failed to insert chart: ${error.message || 'Unknown error'}`);
222
+ }
223
+ },
224
+ });
225
+ }
@@ -0,0 +1,62 @@
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: 'listSpreadsheets',
7
+ description: 'Lists spreadsheets in your Drive, optionally filtered by name or content.',
8
+ parameters: z.object({
9
+ maxResults: z
10
+ .number()
11
+ .int()
12
+ .min(1)
13
+ .max(100)
14
+ .optional()
15
+ .default(20)
16
+ .describe('Maximum number of spreadsheets to return (1-100).'),
17
+ query: z
18
+ .string()
19
+ .optional()
20
+ .describe('Search query to filter spreadsheets by name or content.'),
21
+ orderBy: z
22
+ .enum(['name', 'modifiedTime', 'createdTime'])
23
+ .optional()
24
+ .default('modifiedTime')
25
+ .describe('Sort order for results.'),
26
+ }),
27
+ execute: async (args, { log }) => {
28
+ const drive = await getDriveClient();
29
+ log.info(`Listing Google Sheets. Query: ${args.query || 'none'}, Max: ${args.maxResults}, Order: ${args.orderBy}`);
30
+ try {
31
+ // Build the query string for Google Drive API
32
+ let queryString = "mimeType='application/vnd.google-apps.spreadsheet' and trashed=false";
33
+ if (args.query) {
34
+ queryString += ` and (name contains '${args.query}' or fullText contains '${args.query}')`;
35
+ }
36
+ const response = await drive.files.list({
37
+ q: queryString,
38
+ pageSize: args.maxResults,
39
+ orderBy: args.orderBy === 'name' ? 'name' : args.orderBy,
40
+ fields: 'files(id,name,modifiedTime,createdTime,size,webViewLink,owners(displayName,emailAddress))',
41
+ supportsAllDrives: true,
42
+ includeItemsFromAllDrives: true,
43
+ });
44
+ const files = response.data.files || [];
45
+ const spreadsheets = files.map((file) => ({
46
+ id: file.id,
47
+ name: file.name,
48
+ modifiedTime: file.modifiedTime,
49
+ owner: file.owners?.[0]?.displayName || null,
50
+ url: file.webViewLink,
51
+ }));
52
+ return JSON.stringify({ spreadsheets }, null, 2);
53
+ }
54
+ catch (error) {
55
+ log.error(`Error listing Google Sheets: ${error.message || error}`);
56
+ if (error.code === 403)
57
+ throw new UserError('Permission denied. Make sure you have granted Google Drive access to the application.');
58
+ throw new UserError(`Failed to list spreadsheets: ${error.message || 'Unknown error'}`);
59
+ }
60
+ },
61
+ });
62
+ }
@@ -0,0 +1,55 @@
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: 'listTables',
8
+ description: 'Lists all tables in a spreadsheet or specific sheet. Use this to discover table names and IDs before performing table operations.',
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
+ sheetName: z
14
+ .string()
15
+ .optional()
16
+ .describe('Optional: filter tables to only those on this specific sheet.'),
17
+ }),
18
+ execute: async (args, { log }) => {
19
+ const sheets = await getSheetsClient();
20
+ log.info(`Listing tables for spreadsheet: ${args.spreadsheetId}`);
21
+ try {
22
+ const tables = await SheetsHelpers.listAllTables(sheets, args.spreadsheetId, args.sheetName);
23
+ if (tables.length === 0) {
24
+ return JSON.stringify({
25
+ spreadsheetId: args.spreadsheetId,
26
+ sheetFilter: args.sheetName || 'All sheets',
27
+ tables: [],
28
+ message: 'No tables found. Use createTable to create a table.',
29
+ }, null, 2);
30
+ }
31
+ const tableList = tables.map((item) => ({
32
+ tableId: item.table.tableId,
33
+ name: item.table.name,
34
+ sheetName: item.sheetName,
35
+ columnCount: item.table.columnProperties?.length || 0,
36
+ range: item.table.range
37
+ ? `${item.sheetName}!${SheetsHelpers.rowColToA1(item.table.range.startRowIndex || 0, item.table.range.startColumnIndex || 0)}:${SheetsHelpers.rowColToA1((item.table.range.endRowIndex || 1) - 1, (item.table.range.endColumnIndex || 1) - 1)}`
38
+ : 'Unknown',
39
+ }));
40
+ return JSON.stringify({
41
+ spreadsheetId: args.spreadsheetId,
42
+ sheetFilter: args.sheetName || 'All sheets',
43
+ count: tableList.length,
44
+ tables: tableList,
45
+ }, null, 2);
46
+ }
47
+ catch (error) {
48
+ log.error(`Error listing tables: ${error.message || error}`);
49
+ if (error instanceof UserError)
50
+ throw error;
51
+ throw new UserError(`Failed to list tables: ${error.message || 'Unknown error'}`);
52
+ }
53
+ },
54
+ });
55
+ }
@@ -0,0 +1,143 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSheetsClient } from '../../clients.js';
4
+ import { rowColToA1 } from '../../googleSheetsApiHelpers.js';
5
+ /**
6
+ * Converts a Google Sheets RGBA color object (0-1 range) to a hex string.
7
+ * Returns null if the color is undefined or has no meaningful channels.
8
+ */
9
+ function rgbaToHex(color) {
10
+ if (!color)
11
+ return null;
12
+ const r = Math.round((color.red ?? 0) * 255);
13
+ const g = Math.round((color.green ?? 0) * 255);
14
+ const b = Math.round((color.blue ?? 0) * 255);
15
+ return `#${r.toString(16).padStart(2, '0').toUpperCase()}${g.toString(16).padStart(2, '0').toUpperCase()}${b.toString(16).padStart(2, '0').toUpperCase()}`;
16
+ }
17
+ /**
18
+ * Extracts a simplified formatting summary from a Google Sheets CellFormat object.
19
+ * Only includes properties that are explicitly set (non-default).
20
+ */
21
+ function simplifyFormat(fmt) {
22
+ if (!fmt)
23
+ return null;
24
+ const result = {};
25
+ // Text formatting
26
+ if (fmt.textFormat) {
27
+ const tf = {};
28
+ if (fmt.textFormat.bold)
29
+ tf.bold = true;
30
+ if (fmt.textFormat.italic)
31
+ tf.italic = true;
32
+ if (fmt.textFormat.strikethrough)
33
+ tf.strikethrough = true;
34
+ if (fmt.textFormat.underline)
35
+ tf.underline = true;
36
+ if (fmt.textFormat.fontSize != null)
37
+ tf.fontSize = fmt.textFormat.fontSize;
38
+ if (fmt.textFormat.fontFamily)
39
+ tf.fontFamily = fmt.textFormat.fontFamily;
40
+ if (fmt.textFormat.foregroundColorStyle?.rgbColor) {
41
+ tf.foregroundColor = rgbaToHex(fmt.textFormat.foregroundColorStyle.rgbColor);
42
+ }
43
+ else if (fmt.textFormat.foregroundColor) {
44
+ tf.foregroundColor = rgbaToHex(fmt.textFormat.foregroundColor);
45
+ }
46
+ if (Object.keys(tf).length > 0)
47
+ result.textFormat = tf;
48
+ }
49
+ // Background color
50
+ if (fmt.backgroundColorStyle?.rgbColor) {
51
+ result.backgroundColor = rgbaToHex(fmt.backgroundColorStyle.rgbColor);
52
+ }
53
+ else if (fmt.backgroundColor) {
54
+ result.backgroundColor = rgbaToHex(fmt.backgroundColor);
55
+ }
56
+ // Alignment
57
+ if (fmt.horizontalAlignment)
58
+ result.horizontalAlignment = fmt.horizontalAlignment;
59
+ if (fmt.verticalAlignment)
60
+ result.verticalAlignment = fmt.verticalAlignment;
61
+ // Number format
62
+ if (fmt.numberFormat) {
63
+ result.numberFormat = {
64
+ type: fmt.numberFormat.type,
65
+ pattern: fmt.numberFormat.pattern,
66
+ };
67
+ }
68
+ // Borders
69
+ if (fmt.borders) {
70
+ const borders = {};
71
+ for (const side of ['top', 'bottom', 'left', 'right']) {
72
+ if (fmt.borders[side]) {
73
+ borders[side] = {
74
+ style: fmt.borders[side].style,
75
+ ...(fmt.borders[side].colorStyle?.rgbColor
76
+ ? { color: rgbaToHex(fmt.borders[side].colorStyle.rgbColor) }
77
+ : fmt.borders[side].color
78
+ ? { color: rgbaToHex(fmt.borders[side].color) }
79
+ : {}),
80
+ };
81
+ }
82
+ }
83
+ if (Object.keys(borders).length > 0)
84
+ result.borders = borders;
85
+ }
86
+ // Wrap strategy
87
+ if (fmt.wrapStrategy)
88
+ result.wrapStrategy = fmt.wrapStrategy;
89
+ return Object.keys(result).length > 0 ? result : null;
90
+ }
91
+ export function register(server) {
92
+ server.addTool({
93
+ name: 'readCellFormat',
94
+ description: 'Reads the formatting/style of cells in a given range. Returns formatting details like bold, italic, fontSize, fontFamily, colors, alignment, borders, and number format per cell.',
95
+ parameters: z.object({
96
+ spreadsheetId: z
97
+ .string()
98
+ .describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
99
+ range: z
100
+ .string()
101
+ .describe('A1 notation range to read formatting from (e.g., "Sheet1!A1:D5" or "A1:B2").'),
102
+ }),
103
+ execute: async (args, { log }) => {
104
+ const sheets = await getSheetsClient();
105
+ log.info(`Reading cell format for range "${args.range}" in spreadsheet ${args.spreadsheetId}`);
106
+ try {
107
+ const response = await sheets.spreadsheets.get({
108
+ spreadsheetId: args.spreadsheetId,
109
+ ranges: [args.range],
110
+ includeGridData: true,
111
+ fields: 'sheets.data.rowData.values.userEnteredFormat,sheets.data.startRow,sheets.data.startColumn',
112
+ });
113
+ const sheetData = response.data.sheets?.[0]?.data?.[0];
114
+ if (!sheetData?.rowData) {
115
+ return JSON.stringify({ range: args.range, cells: [] }, null, 2);
116
+ }
117
+ const startRow = sheetData.startRow ?? 0;
118
+ const startCol = sheetData.startColumn ?? 0;
119
+ const cells = [];
120
+ for (let rowIdx = 0; rowIdx < sheetData.rowData.length; rowIdx++) {
121
+ const row = sheetData.rowData[rowIdx];
122
+ if (!row.values)
123
+ continue;
124
+ for (let colIdx = 0; colIdx < row.values.length; colIdx++) {
125
+ const cellData = row.values[colIdx];
126
+ const fmt = simplifyFormat(cellData?.userEnteredFormat);
127
+ if (fmt) {
128
+ const cellRef = rowColToA1(startRow + rowIdx, startCol + colIdx);
129
+ cells.push({ cell: cellRef, format: fmt });
130
+ }
131
+ }
132
+ }
133
+ return JSON.stringify({ range: args.range, cells }, null, 2);
134
+ }
135
+ catch (error) {
136
+ log.error(`Error reading cell format for spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
137
+ if (error instanceof UserError)
138
+ throw error;
139
+ throw new UserError(`Failed to read cell format: ${error.message || 'Unknown error'}`);
140
+ }
141
+ },
142
+ });
143
+ }