@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,42 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient } from '../../clients.js';
4
+ import { DocumentIdParameter } from '../../types.js';
5
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
6
+ export function register(server) {
7
+ server.addTool({
8
+ name: 'insertTable',
9
+ description: 'Inserts an empty table with the specified number of rows and columns at a character index in the document.',
10
+ parameters: DocumentIdParameter.extend({
11
+ rows: z.number().int().min(1).describe('Number of rows for the new table.'),
12
+ columns: z.number().int().min(1).describe('Number of columns for the new table.'),
13
+ index: z
14
+ .number()
15
+ .int()
16
+ .min(1)
17
+ .describe("1-based character index within the document body. Use readDocument with format='json' to inspect indices."),
18
+ tabId: z
19
+ .string()
20
+ .optional()
21
+ .describe('The ID of the specific tab to insert into. Use listDocumentTabs to get tab IDs. If not specified, inserts into the first tab.'),
22
+ }),
23
+ execute: async (args, { log }) => {
24
+ const docs = await getDocsClient();
25
+ log.info(`Inserting ${args.rows}x${args.columns} table in doc ${args.documentId} at index ${args.index}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
26
+ try {
27
+ // If tabId is specified, verify the tab exists
28
+ if (args.tabId) {
29
+ const targetTab = await GDocsHelpers.getDocumentTab(docs, args.documentId, args.tabId);
30
+ }
31
+ await GDocsHelpers.createTable(docs, args.documentId, args.rows, args.columns, args.index, args.tabId);
32
+ return `Successfully inserted a ${args.rows}x${args.columns} table at index ${args.index}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
33
+ }
34
+ catch (error) {
35
+ log.error(`Error inserting table in doc ${args.documentId}: ${error.message || error}`);
36
+ if (error instanceof UserError)
37
+ throw error;
38
+ throw new UserError(`Failed to insert table: ${error.message || 'Unknown error'}`);
39
+ }
40
+ },
41
+ });
42
+ }
@@ -0,0 +1,125 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient } from '../../clients.js';
4
+ import { DocumentIdParameter } from '../../types.js';
5
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
6
+ // --- Table Index Math ---
7
+ // Google Docs API table index layout for an R×C table inserted at T:
8
+ // cellContentIndex(T, r, c, C) = T + 4 + r * (1 + 2*C) + 2*c
9
+ // (Verified empirically; see markdownToDocs.ts lines 640-658)
10
+ /**
11
+ * Builds the Google Docs API requests to insert a table and populate its cells.
12
+ * Exported for testability (same pattern as convertMarkdownToRequests).
13
+ */
14
+ export function buildInsertTableWithDataRequests(data, index, hasHeaderRow, tabId) {
15
+ const numRows = data.length;
16
+ const numCols = data.reduce((max, row) => Math.max(max, row.length), 0);
17
+ if (numRows === 0 || numCols === 0) {
18
+ throw new UserError('Table data must contain at least one non-empty row with at least one cell.');
19
+ }
20
+ // Pad ragged rows to uniform column count
21
+ const normalizedData = data.map((row) => {
22
+ const padded = [...row];
23
+ while (padded.length < numCols)
24
+ padded.push('');
25
+ return padded;
26
+ });
27
+ const insertRequests = [];
28
+ const formatRequests = [];
29
+ // 1. Insert the empty table structure
30
+ const location = { index };
31
+ if (tabId)
32
+ location.tabId = tabId;
33
+ insertRequests.push({
34
+ insertTable: {
35
+ location: location,
36
+ rows: numRows,
37
+ columns: numCols,
38
+ },
39
+ });
40
+ // 2. Insert text into each cell, tracking cumulative offset
41
+ let cumulativeTextLength = 0;
42
+ for (let r = 0; r < numRows; r++) {
43
+ for (let c = 0; c < numCols; c++) {
44
+ const cellText = normalizedData[r][c];
45
+ if (!cellText)
46
+ continue;
47
+ const baseCellIndex = index + 4 + r * (1 + 2 * numCols) + 2 * c;
48
+ const adjustedIndex = baseCellIndex + cumulativeTextLength;
49
+ const cellLocation = { index: adjustedIndex };
50
+ if (tabId)
51
+ cellLocation.tabId = tabId;
52
+ insertRequests.push({
53
+ insertText: {
54
+ location: cellLocation,
55
+ text: cellText,
56
+ },
57
+ });
58
+ // 3. Bold header row cells
59
+ if (hasHeaderRow && r === 0) {
60
+ const styleReq = GDocsHelpers.buildUpdateTextStyleRequest(adjustedIndex, adjustedIndex + cellText.length, { bold: true }, tabId);
61
+ if (styleReq)
62
+ formatRequests.push(styleReq.request);
63
+ }
64
+ cumulativeTextLength += cellText.length;
65
+ }
66
+ }
67
+ return [...insertRequests, ...formatRequests];
68
+ }
69
+ export function register(server) {
70
+ server.addTool({
71
+ name: 'insertTableWithData',
72
+ description: 'Inserts a table pre-populated with data at a specific index in the document. ' +
73
+ 'All cell content is inserted in a single operation. ' +
74
+ 'Optionally bolds the first row as a header. ' +
75
+ 'Ragged rows are padded with empty cells to match the widest row.',
76
+ parameters: DocumentIdParameter.extend({
77
+ data: z
78
+ .array(z.array(z.string()).max(50))
79
+ .min(1)
80
+ .max(200)
81
+ .describe('A 2D array of strings representing the table contents. Each inner array is one row. ' +
82
+ 'Example: [["Name", "Age"], ["Alice", "30"], ["Bob", "25"]]'),
83
+ index: z
84
+ .number()
85
+ .int()
86
+ .min(1)
87
+ .describe('1-based character index within the document body where the table should be inserted. ' +
88
+ "Use readDocument with format='json' to inspect indices."),
89
+ hasHeaderRow: z
90
+ .boolean()
91
+ .optional()
92
+ .default(false)
93
+ .describe('If true, the first row is treated as a header and its text will be bolded.'),
94
+ tabId: z
95
+ .string()
96
+ .optional()
97
+ .describe('The ID of the specific tab to insert into. Use listDocumentTabs to get tab IDs. ' +
98
+ 'If not specified, inserts into the first tab.'),
99
+ }),
100
+ execute: async (args, { log }) => {
101
+ const docs = await getDocsClient();
102
+ const numRows = args.data.length;
103
+ const numCols = args.data.reduce((max, row) => Math.max(max, row.length), 0);
104
+ log.info(`Inserting ${numRows}x${numCols} table with data in doc ${args.documentId} at index ${args.index}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
105
+ try {
106
+ // Validate tab if specified (same pattern as insertTable.ts)
107
+ if (args.tabId) {
108
+ const targetTab = await GDocsHelpers.getDocumentTab(docs, args.documentId, args.tabId);
109
+ }
110
+ const requests = buildInsertTableWithDataRequests(args.data, args.index, args.hasHeaderRow ?? false, args.tabId);
111
+ const metadata = await GDocsHelpers.executeBatchUpdateWithSplitting(docs, args.documentId, requests, log);
112
+ return (`Successfully inserted a ${numRows}x${numCols} table with data at index ${args.index}` +
113
+ `${args.tabId ? ` in tab ${args.tabId}` : ''}. ` +
114
+ `${args.hasHeaderRow ? 'Header row bolded. ' : ''}` +
115
+ `(${metadata.totalRequests} requests in ${metadata.totalApiCalls} API calls, ${metadata.totalElapsedMs}ms)`);
116
+ }
117
+ catch (error) {
118
+ log.error(`Error inserting table with data in doc ${args.documentId}: ${error.message || error}`);
119
+ if (error instanceof UserError)
120
+ throw error;
121
+ throw new UserError(`Failed to insert table with data: ${error.message || 'Unknown error'}`);
122
+ }
123
+ },
124
+ });
125
+ }
@@ -0,0 +1,49 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient } from '../../clients.js';
4
+ import { DocumentIdParameter } from '../../types.js';
5
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
6
+ export function register(server) {
7
+ server.addTool({
8
+ name: 'insertText',
9
+ description: "Inserts text at a specific character index within a document. Use readDocument with format='json' to determine the correct index.",
10
+ parameters: DocumentIdParameter.extend({
11
+ text: z.string().min(1).describe('The text to insert.'),
12
+ index: z
13
+ .number()
14
+ .int()
15
+ .min(1)
16
+ .describe("1-based character index within the document body. Use readDocument with format='json' to inspect indices."),
17
+ tabId: z
18
+ .string()
19
+ .optional()
20
+ .describe('The ID of the specific tab to insert into. If not specified, inserts into the first tab (or legacy document.body for documents without tabs).'),
21
+ }),
22
+ execute: async (args, { log }) => {
23
+ const docs = await getDocsClient();
24
+ log.info(`Inserting text in doc ${args.documentId} at index ${args.index}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
25
+ try {
26
+ if (args.tabId) {
27
+ const targetTab = await GDocsHelpers.getDocumentTab(docs, args.documentId, args.tabId);
28
+ // Insert with tabId
29
+ const location = { index: args.index, tabId: args.tabId };
30
+ const request = {
31
+ insertText: { location, text: args.text },
32
+ };
33
+ await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [request]);
34
+ }
35
+ else {
36
+ // Use existing helper for backward compatibility
37
+ await GDocsHelpers.insertText(docs, args.documentId, args.text, args.index);
38
+ }
39
+ return `Successfully inserted text at index ${args.index}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
40
+ }
41
+ catch (error) {
42
+ log.error(`Error inserting text in doc ${args.documentId}: ${error.message || error}`);
43
+ if (error instanceof UserError)
44
+ throw error;
45
+ throw new UserError(`Failed to insert text: ${error.message || 'Unknown error'}`);
46
+ }
47
+ },
48
+ });
49
+ }
@@ -0,0 +1,47 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient } from '../../clients.js';
4
+ import { DocumentIdParameter } from '../../types.js';
5
+ import { extractDocumentTables } from './structureHelpers.js';
6
+ import { TABLE_CONTENT_BASIC_BODY_FIELDS, buildDocumentGetFields } from './tabFieldMasks.js';
7
+ export function register(server) {
8
+ server.addTool({
9
+ name: 'listDocumentTables',
10
+ description: 'Lists tables in a Google Document with stable MCP table IDs, ranges, and dimensions. Use this before table-specific editing tools.',
11
+ parameters: DocumentIdParameter.extend({
12
+ tabId: z
13
+ .string()
14
+ .optional()
15
+ .describe('The ID of the specific tab to inspect. If not specified, inspects the first tab or legacy document body.'),
16
+ }),
17
+ execute: async (args, { log }) => {
18
+ const docs = await getDocsClient();
19
+ log.info(`Listing document tables for ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
20
+ try {
21
+ const res = await docs.documents.get({
22
+ documentId: args.documentId,
23
+ ...(args.tabId && { includeTabsContent: true }),
24
+ fields: buildDocumentGetFields(TABLE_CONTENT_BASIC_BODY_FIELDS, args.tabId),
25
+ });
26
+ const tables = extractDocumentTables(res.data, args.tabId).map((table) => ({
27
+ tableId: table.tableId,
28
+ startIndex: table.startIndex,
29
+ endIndex: table.endIndex,
30
+ rowCount: table.rowCount,
31
+ columnCount: table.columnCount,
32
+ }));
33
+ return JSON.stringify({ tables }, null, 2);
34
+ }
35
+ catch (error) {
36
+ log.error(`Error listing tables for doc ${args.documentId}: ${error.message || error}`);
37
+ if (error instanceof UserError)
38
+ throw error;
39
+ if (error.code === 404)
40
+ throw new UserError(`Document not found (ID: ${args.documentId}).`);
41
+ if (error.code === 403)
42
+ throw new UserError(`Permission denied for document (ID: ${args.documentId}).`);
43
+ throw new UserError(`Failed to list document tables: ${error.message || 'Unknown error'}`);
44
+ }
45
+ },
46
+ });
47
+ }
@@ -0,0 +1,59 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient } from '../../clients.js';
4
+ import { DocumentIdParameter } from '../../types.js';
5
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
6
+ import { TAB_LIST_FIELDS, TAB_LIST_WITH_CONTENT_FIELDS } from './tabFieldMasks.js';
7
+ export function register(server) {
8
+ server.addTool({
9
+ name: 'listTabs',
10
+ description: "Lists all tabs in a document with their IDs and hierarchy. Use the returned tab IDs with other tools' tabId parameter to target a specific tab.",
11
+ parameters: DocumentIdParameter.extend({
12
+ includeContent: z
13
+ .boolean()
14
+ .optional()
15
+ .default(false)
16
+ .describe('Whether to include a content summary for each tab (character count).'),
17
+ }),
18
+ execute: async (args, { log }) => {
19
+ const docs = await getDocsClient();
20
+ log.info(`Listing tabs for document: ${args.documentId}`);
21
+ try {
22
+ // Get document with tabs structure
23
+ const res = await docs.documents.get({
24
+ documentId: args.documentId,
25
+ includeTabsContent: true,
26
+ // Only get essential fields for tab listing
27
+ fields: args.includeContent ? TAB_LIST_WITH_CONTENT_FIELDS : TAB_LIST_FIELDS,
28
+ });
29
+ const docTitle = res.data.title || 'Untitled Document';
30
+ // Get all tabs in a flat list with hierarchy info
31
+ const allTabs = GDocsHelpers.getAllTabs(res.data);
32
+ const tabs = allTabs.map((tab) => {
33
+ const tabProperties = tab.tabProperties || {};
34
+ const tabObj = {
35
+ id: tabProperties.tabId || null,
36
+ title: tabProperties.title || null,
37
+ index: tabProperties.index ?? null,
38
+ };
39
+ if (tabProperties.parentTabId) {
40
+ tabObj.parentTabId = tabProperties.parentTabId;
41
+ }
42
+ if (args.includeContent && tab.documentTab) {
43
+ tabObj.characterCount = GDocsHelpers.getTabTextLength(tab.documentTab);
44
+ }
45
+ return tabObj;
46
+ });
47
+ return JSON.stringify({ documentTitle: docTitle, tabs }, null, 2);
48
+ }
49
+ catch (error) {
50
+ log.error(`Error listing tabs for doc ${args.documentId}: ${error.message || error}`);
51
+ if (error.code === 404)
52
+ throw new UserError(`Document not found (ID: ${args.documentId}).`);
53
+ if (error.code === 403)
54
+ throw new UserError(`Permission denied for document (ID: ${args.documentId}).`);
55
+ throw new UserError(`Failed to list tabs: ${error.message || 'Unknown error'}`);
56
+ }
57
+ },
58
+ });
59
+ }
@@ -0,0 +1,41 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient } from '../../clients.js';
4
+ import { DocumentIdParameter } from '../../types.js';
5
+ import { extractSmartChips } from './smartChipHelpers.js';
6
+ import { SMART_CHIP_BODY_FIELDS, buildDocumentGetFields } from './tabFieldMasks.js';
7
+ export function register(server) {
8
+ server.addTool({
9
+ name: 'listSmartChips',
10
+ description: 'Lists smart chips in a Google Document, including date elements, person mentions, and rich links.',
11
+ parameters: DocumentIdParameter.extend({
12
+ tabId: z
13
+ .string()
14
+ .optional()
15
+ .describe('The ID of the specific tab to inspect. If not specified, inspects the first tab or legacy document body.'),
16
+ }),
17
+ execute: async (args, { log }) => {
18
+ const docs = await getDocsClient();
19
+ log.info(`Listing smart chips for ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
20
+ try {
21
+ const res = await docs.documents.get({
22
+ documentId: args.documentId,
23
+ ...(args.tabId && { includeTabsContent: true }),
24
+ fields: buildDocumentGetFields(SMART_CHIP_BODY_FIELDS, args.tabId),
25
+ });
26
+ const chips = extractSmartChips(res.data, args.tabId);
27
+ return JSON.stringify({ smartChips: chips }, null, 2);
28
+ }
29
+ catch (error) {
30
+ log.error(`Error listing smart chips for doc ${args.documentId}: ${error.message || error}`);
31
+ if (error instanceof UserError)
32
+ throw error;
33
+ if (error.code === 404)
34
+ throw new UserError(`Document not found (ID: ${args.documentId}).`);
35
+ if (error.code === 403)
36
+ throw new UserError(`Permission denied for document (ID: ${args.documentId}).`);
37
+ throw new UserError(`Failed to list smart chips: ${error.message || 'Unknown error'}`);
38
+ }
39
+ },
40
+ });
41
+ }
@@ -0,0 +1,147 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient } from '../../clients.js';
4
+ import { DocumentIdParameter, TextFindParameter, TextStyleParameters } from '../../types.js';
5
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
6
+ const RangeTarget = z
7
+ .object({
8
+ startIndex: z.number().int().min(1).describe('Start of range (inclusive, 1-based).'),
9
+ endIndex: z.number().int().min(1).describe('End of range (exclusive).'),
10
+ })
11
+ .refine((d) => d.endIndex > d.startIndex, {
12
+ message: 'endIndex must be greater than startIndex',
13
+ path: ['endIndex'],
14
+ });
15
+ const InsertionTarget = z.strictObject({
16
+ insertionIndex: z.number().int().min(1).describe('Index to insert at (1-based).'),
17
+ });
18
+ const ModifyTextParameters = DocumentIdParameter.extend({
19
+ target: z
20
+ .union([RangeTarget, TextFindParameter, InsertionTarget])
21
+ .describe('Target by range indices, text search, or insertion index.'),
22
+ text: z.string().optional().describe('New text to insert or replace with.'),
23
+ style: TextStyleParameters.optional().describe('Text formatting to apply.'),
24
+ tabId: z
25
+ .string()
26
+ .optional()
27
+ .describe('The ID of the specific tab to operate on. If not specified, operates on the first tab.'),
28
+ })
29
+ .refine((args) => args.text !== undefined || args.style !== undefined, {
30
+ message: 'At least one of text or style must be provided.',
31
+ })
32
+ .refine((args) => {
33
+ if ('insertionIndex' in args.target && args.text === undefined)
34
+ return false;
35
+ return true;
36
+ }, { message: 'text is required when using insertionIndex target (no existing range to format).' });
37
+ /**
38
+ * Pure, sync function that builds the array of Google Docs API requests for a
39
+ * modifyText operation. Indices must already be resolved (no text-search here).
40
+ */
41
+ export function buildModifyTextRequests(opts) {
42
+ const { startIndex, endIndex, text, style, tabId } = opts;
43
+ const requests = [];
44
+ if (!text && !style)
45
+ return requests;
46
+ // 1. Delete existing content (only when replacing, not insert-only)
47
+ if (endIndex !== undefined && text !== undefined) {
48
+ const range = { startIndex, endIndex };
49
+ if (tabId)
50
+ range.tabId = tabId;
51
+ requests.push({ deleteContentRange: { range } });
52
+ }
53
+ // 2. Insert new text
54
+ if (text !== undefined) {
55
+ const location = { index: startIndex };
56
+ if (tabId)
57
+ location.tabId = tabId;
58
+ requests.push({ insertText: { location, text } });
59
+ }
60
+ // 3. Apply formatting
61
+ if (style) {
62
+ const formatStart = startIndex;
63
+ const formatEnd = text !== undefined
64
+ ? startIndex + text.length
65
+ : endIndex !== undefined
66
+ ? endIndex
67
+ : startIndex;
68
+ if (formatEnd > formatStart) {
69
+ const requestInfo = GDocsHelpers.buildUpdateTextStyleRequest(formatStart, formatEnd, style, tabId);
70
+ if (requestInfo) {
71
+ requests.push(requestInfo.request);
72
+ }
73
+ }
74
+ }
75
+ return requests;
76
+ }
77
+ export function register(server) {
78
+ server.addTool({
79
+ name: 'modifyText',
80
+ description: 'Combines text replacement/insertion and formatting in one atomic operation. ' +
81
+ 'Can insert text at a position, replace a range or found text, apply formatting, ' +
82
+ "or any combination. Use readGoogleDoc with format='json' to determine indices.",
83
+ parameters: ModifyTextParameters,
84
+ execute: async (args, { log }) => {
85
+ const docs = await getDocsClient();
86
+ log.info(`modifyText on doc ${args.documentId}: target=${JSON.stringify(args.target)}` +
87
+ `${args.text !== undefined ? `, text="${args.text.substring(0, 50)}"` : ''}` +
88
+ `${args.style ? `, style=${JSON.stringify(args.style)}` : ''}` +
89
+ `${args.tabId ? `, tab=${args.tabId}` : ''}`);
90
+ try {
91
+ // Verify tab exists if specified
92
+ if (args.tabId) {
93
+ const targetTab = await GDocsHelpers.getDocumentTab(docs, args.documentId, args.tabId);
94
+ }
95
+ // Resolve target to numeric indices
96
+ let startIndex;
97
+ let endIndex;
98
+ if ('insertionIndex' in args.target) {
99
+ startIndex = args.target.insertionIndex;
100
+ endIndex = undefined;
101
+ }
102
+ else if ('textToFind' in args.target) {
103
+ const range = await GDocsHelpers.findTextRange(docs, args.documentId, args.target.textToFind, args.target.matchInstance, args.tabId);
104
+ if (!range) {
105
+ throw new UserError(`Could not find instance ${args.target.matchInstance ?? 1} of text "${args.target.textToFind}"${args.tabId ? ` in tab ${args.tabId}` : ''}.`);
106
+ }
107
+ startIndex = range.startIndex;
108
+ endIndex = range.endIndex;
109
+ log.info(`Found text "${args.target.textToFind}" at range ${startIndex}-${endIndex}`);
110
+ }
111
+ else {
112
+ startIndex = args.target.startIndex;
113
+ endIndex = args.target.endIndex;
114
+ }
115
+ // Clamp to minimum 1 (index 0 is the document section break)
116
+ if (startIndex < 1)
117
+ startIndex = 1;
118
+ const requests = buildModifyTextRequests({
119
+ startIndex,
120
+ endIndex,
121
+ text: args.text,
122
+ style: args.style,
123
+ tabId: args.tabId,
124
+ });
125
+ if (requests.length === 0) {
126
+ return 'No operations to perform.';
127
+ }
128
+ await GDocsHelpers.executeBatchUpdate(docs, args.documentId, requests);
129
+ // Build descriptive result
130
+ const actions = [];
131
+ if (endIndex !== undefined && args.text !== undefined)
132
+ actions.push('replaced text');
133
+ else if (args.text !== undefined)
134
+ actions.push('inserted text');
135
+ if (args.style)
136
+ actions.push('applied formatting');
137
+ return `Successfully ${actions.join(' and ')} at range ${startIndex}-${endIndex ?? startIndex + (args.text?.length ?? 0)}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
138
+ }
139
+ catch (error) {
140
+ log.error(`Error in modifyText for doc ${args.documentId}: ${error.message || error}`);
141
+ if (error instanceof UserError)
142
+ throw error;
143
+ throw new UserError(`Failed to modify text: ${error.message || 'Unknown error'}`);
144
+ }
145
+ },
146
+ });
147
+ }