google-tools-mcp 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 (95) hide show
  1. package/dist/auth.js +275 -0
  2. package/dist/cachedToolsList.js +52 -0
  3. package/dist/clients.js +97 -0
  4. package/dist/googleDocsApiHelpers.js +883 -0
  5. package/dist/googleSheetsApiHelpers.js +808 -0
  6. package/dist/helpers.js +210 -0
  7. package/dist/index.js +55 -0
  8. package/dist/logger.js +45 -0
  9. package/dist/markdown-transformer/docsToMarkdown.js +259 -0
  10. package/dist/markdown-transformer/index.js +126 -0
  11. package/dist/markdown-transformer/markdownToDocs.js +834 -0
  12. package/dist/tools/docs/addTab.js +92 -0
  13. package/dist/tools/docs/appendToGoogleDoc.js +81 -0
  14. package/dist/tools/docs/comments/addComment.js +83 -0
  15. package/dist/tools/docs/comments/deleteComment.js +30 -0
  16. package/dist/tools/docs/comments/getComment.js +45 -0
  17. package/dist/tools/docs/comments/index.js +14 -0
  18. package/dist/tools/docs/comments/listComments.js +43 -0
  19. package/dist/tools/docs/comments/replyToComment.js +35 -0
  20. package/dist/tools/docs/comments/resolveComment.js +55 -0
  21. package/dist/tools/docs/deleteRange.js +72 -0
  22. package/dist/tools/docs/findAndReplace.js +54 -0
  23. package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
  24. package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
  25. package/dist/tools/docs/formatting/index.js +6 -0
  26. package/dist/tools/docs/index.js +38 -0
  27. package/dist/tools/docs/insertImage.js +122 -0
  28. package/dist/tools/docs/insertPageBreak.js +58 -0
  29. package/dist/tools/docs/insertTable.js +53 -0
  30. package/dist/tools/docs/insertTableWithData.js +135 -0
  31. package/dist/tools/docs/insertText.js +61 -0
  32. package/dist/tools/docs/listDocumentTabs.js +60 -0
  33. package/dist/tools/docs/modifyText.js +158 -0
  34. package/dist/tools/docs/readGoogleDoc.js +165 -0
  35. package/dist/tools/docs/renameTab.js +61 -0
  36. package/dist/tools/drive/copyFile.js +63 -0
  37. package/dist/tools/drive/createDocument.js +89 -0
  38. package/dist/tools/drive/createFolder.js +48 -0
  39. package/dist/tools/drive/createFromTemplate.js +82 -0
  40. package/dist/tools/drive/deleteFile.js +72 -0
  41. package/dist/tools/drive/getDocumentInfo.js +48 -0
  42. package/dist/tools/drive/getFolderInfo.js +48 -0
  43. package/dist/tools/drive/index.js +30 -0
  44. package/dist/tools/drive/listFolderContents.js +82 -0
  45. package/dist/tools/drive/listGoogleDocs.js +67 -0
  46. package/dist/tools/drive/listSharedDrives.js +52 -0
  47. package/dist/tools/drive/listSharedWithMe.js +97 -0
  48. package/dist/tools/drive/moveFile.js +54 -0
  49. package/dist/tools/drive/renameFile.js +39 -0
  50. package/dist/tools/drive/searchGoogleDocs.js +98 -0
  51. package/dist/tools/extras/index.js +7 -0
  52. package/dist/tools/extras/readFile.js +82 -0
  53. package/dist/tools/extras/searchFileContents.js +81 -0
  54. package/dist/tools/gmail/drafts.js +165 -0
  55. package/dist/tools/gmail/labels.js +103 -0
  56. package/dist/tools/gmail/messages.js +448 -0
  57. package/dist/tools/gmail/settings.js +528 -0
  58. package/dist/tools/gmail/threads.js +145 -0
  59. package/dist/tools/index.js +163 -0
  60. package/dist/tools/sheets/addConditionalFormatting.js +143 -0
  61. package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
  62. package/dist/tools/sheets/appendSpreadsheetRows.js +43 -0
  63. package/dist/tools/sheets/appendTableRows.js +50 -0
  64. package/dist/tools/sheets/autoResizeColumns.js +67 -0
  65. package/dist/tools/sheets/batchWrite.js +59 -0
  66. package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
  67. package/dist/tools/sheets/copyFormatting.js +59 -0
  68. package/dist/tools/sheets/createSpreadsheet.js +71 -0
  69. package/dist/tools/sheets/createTable.js +120 -0
  70. package/dist/tools/sheets/deleteChart.js +41 -0
  71. package/dist/tools/sheets/deleteSheet.js +43 -0
  72. package/dist/tools/sheets/deleteTable.js +56 -0
  73. package/dist/tools/sheets/duplicateSheet.js +53 -0
  74. package/dist/tools/sheets/formatCells.js +106 -0
  75. package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
  76. package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
  77. package/dist/tools/sheets/getTable.js +48 -0
  78. package/dist/tools/sheets/groupRows.js +62 -0
  79. package/dist/tools/sheets/index.js +66 -0
  80. package/dist/tools/sheets/insertChart.js +225 -0
  81. package/dist/tools/sheets/listGoogleSheets.js +62 -0
  82. package/dist/tools/sheets/listTables.js +55 -0
  83. package/dist/tools/sheets/readCellFormat.js +143 -0
  84. package/dist/tools/sheets/readSpreadsheet.js +36 -0
  85. package/dist/tools/sheets/renameSheet.js +48 -0
  86. package/dist/tools/sheets/setColumnWidths.js +43 -0
  87. package/dist/tools/sheets/setDropdownValidation.js +51 -0
  88. package/dist/tools/sheets/ungroupAllRows.js +66 -0
  89. package/dist/tools/sheets/updateTableRange.js +51 -0
  90. package/dist/tools/sheets/writeSpreadsheet.js +43 -0
  91. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +93 -0
  92. package/dist/tools/utils/index.js +6 -0
  93. package/dist/tools/utils/replaceDocumentWithMarkdown.js +154 -0
  94. package/dist/types.js +186 -0
  95. package/package.json +30 -0
@@ -0,0 +1,83 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { getDocsClient } from '../../../clients.js';
3
+ import { ApplyParagraphStyleToolParameters, NotImplementedError, } from '../../../types.js';
4
+ import * as GDocsHelpers from '../../../googleDocsApiHelpers.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'applyParagraphStyle',
8
+ description: 'Applies paragraph-level formatting (alignment, spacing, heading styles) to paragraphs identified by a character range or by searching for text. Use namedStyleType to set heading levels (HEADING_1 through HEADING_6).',
9
+ parameters: ApplyParagraphStyleToolParameters,
10
+ execute: async (args, { log }) => {
11
+ const docs = await getDocsClient();
12
+ let startIndex;
13
+ let endIndex;
14
+ log.info(`Applying paragraph style to document ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
15
+ log.info(`Style options: ${JSON.stringify(args.style)}`);
16
+ log.info(`Target specification: ${JSON.stringify(args.target)}`);
17
+ try {
18
+ // STEP 1: Determine the target paragraph's range based on the targeting method
19
+ if ('textToFind' in args.target) {
20
+ // Find the text first
21
+ log.info(`Finding text "${args.target.textToFind}" (instance ${args.target.matchInstance || 1})${args.tabId ? ` in tab ${args.tabId}` : ''}`);
22
+ const textRange = await GDocsHelpers.findTextRange(docs, args.documentId, args.target.textToFind, args.target.matchInstance || 1, args.tabId);
23
+ if (!textRange) {
24
+ throw new UserError(`Could not find "${args.target.textToFind}" in the document${args.tabId ? ` (tab: ${args.tabId})` : ''}.`);
25
+ }
26
+ log.info(`Found text at range ${textRange.startIndex}-${textRange.endIndex}, now locating containing paragraph`);
27
+ // Then find the paragraph containing this text
28
+ const paragraphRange = await GDocsHelpers.getParagraphRange(docs, args.documentId, textRange.startIndex, args.tabId);
29
+ if (!paragraphRange) {
30
+ throw new UserError(`Found the text but could not determine the paragraph boundaries.`);
31
+ }
32
+ startIndex = paragraphRange.startIndex;
33
+ endIndex = paragraphRange.endIndex;
34
+ log.info(`Text is contained within paragraph at range ${startIndex}-${endIndex}`);
35
+ }
36
+ else if ('indexWithinParagraph' in args.target) {
37
+ // Find paragraph containing the specified index
38
+ log.info(`Finding paragraph containing index ${args.target.indexWithinParagraph}${args.tabId ? ` in tab ${args.tabId}` : ''}`);
39
+ const paragraphRange = await GDocsHelpers.getParagraphRange(docs, args.documentId, args.target.indexWithinParagraph, args.tabId);
40
+ if (!paragraphRange) {
41
+ throw new UserError(`Could not find paragraph containing index ${args.target.indexWithinParagraph}${args.tabId ? ` in tab ${args.tabId}` : ''}.`);
42
+ }
43
+ startIndex = paragraphRange.startIndex;
44
+ endIndex = paragraphRange.endIndex;
45
+ log.info(`Located paragraph at range ${startIndex}-${endIndex}`);
46
+ }
47
+ else if ('startIndex' in args.target && 'endIndex' in args.target) {
48
+ // Use directly provided range
49
+ startIndex = args.target.startIndex;
50
+ endIndex = args.target.endIndex;
51
+ log.info(`Using provided paragraph range ${startIndex}-${endIndex}`);
52
+ }
53
+ // Verify that we have a valid range
54
+ if (startIndex === undefined || endIndex === undefined) {
55
+ throw new UserError('Could not determine target paragraph range from the provided information.');
56
+ }
57
+ if (endIndex <= startIndex) {
58
+ throw new UserError(`Invalid paragraph range: end index (${endIndex}) must be greater than start index (${startIndex}).`);
59
+ }
60
+ // STEP 2: Build and apply the paragraph style request
61
+ log.info(`Building paragraph style request for range ${startIndex}-${endIndex}`);
62
+ const requestInfo = GDocsHelpers.buildUpdateParagraphStyleRequest(startIndex, endIndex, args.style, args.tabId);
63
+ if (!requestInfo) {
64
+ return 'No valid paragraph styling options were provided.';
65
+ }
66
+ log.info(`Applying styles: ${requestInfo.fields.join(', ')}`);
67
+ await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [requestInfo.request]);
68
+ return `Successfully applied paragraph styles (${requestInfo.fields.join(', ')}) to the paragraph${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
69
+ }
70
+ catch (error) {
71
+ // Detailed error logging
72
+ log.error(`Error applying paragraph style in doc ${args.documentId}:`);
73
+ log.error(error.stack || error.message || error);
74
+ if (error instanceof UserError)
75
+ throw error;
76
+ if (error instanceof NotImplementedError)
77
+ throw error;
78
+ // Provide a more helpful error message
79
+ throw new UserError(`Failed to apply paragraph style: ${error.message || 'Unknown error'}`);
80
+ }
81
+ },
82
+ });
83
+ }
@@ -0,0 +1,49 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { getDocsClient } from '../../../clients.js';
3
+ import { ApplyTextStyleToolParameters, NotImplementedError, } from '../../../types.js';
4
+ import * as GDocsHelpers from '../../../googleDocsApiHelpers.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'applyTextStyle',
8
+ description: 'Applies character-level formatting (bold, italic, color, font, etc.) to text identified by a character range or by searching for a text string. This is the primary tool for styling text in a document.',
9
+ parameters: ApplyTextStyleToolParameters,
10
+ execute: async (args, { log }) => {
11
+ const docs = await getDocsClient();
12
+ let { startIndex, endIndex } = args.target; // Will be updated if target is text
13
+ log.info(`Applying text style in doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}. Target: ${JSON.stringify(args.target)}, Style: ${JSON.stringify(args.style)}`);
14
+ try {
15
+ // Determine target range
16
+ if ('textToFind' in args.target) {
17
+ const range = await GDocsHelpers.findTextRange(docs, args.documentId, args.target.textToFind, args.target.matchInstance, args.tabId);
18
+ if (!range) {
19
+ throw new UserError(`Could not find instance ${args.target.matchInstance} of text "${args.target.textToFind}"${args.tabId ? ` in tab ${args.tabId}` : ''}.`);
20
+ }
21
+ startIndex = range.startIndex;
22
+ endIndex = range.endIndex;
23
+ log.info(`Found text "${args.target.textToFind}" (instance ${args.target.matchInstance}) at range ${startIndex}-${endIndex}`);
24
+ }
25
+ if (startIndex === undefined || endIndex === undefined) {
26
+ throw new UserError('Target range could not be determined.');
27
+ }
28
+ if (endIndex <= startIndex) {
29
+ throw new UserError('End index must be greater than start index for styling.');
30
+ }
31
+ // Build the request
32
+ const requestInfo = GDocsHelpers.buildUpdateTextStyleRequest(startIndex, endIndex, args.style, args.tabId);
33
+ if (!requestInfo) {
34
+ return 'No valid text styling options were provided.';
35
+ }
36
+ await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [requestInfo.request]);
37
+ return `Successfully applied text style (${requestInfo.fields.join(', ')}) to range ${startIndex}-${endIndex}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
38
+ }
39
+ catch (error) {
40
+ log.error(`Error applying text style in doc ${args.documentId}: ${error.message || error}`);
41
+ if (error instanceof UserError)
42
+ throw error;
43
+ if (error instanceof NotImplementedError)
44
+ throw error; // Should not happen here
45
+ throw new UserError(`Failed to apply text style: ${error.message || 'Unknown error'}`);
46
+ }
47
+ },
48
+ });
49
+ }
@@ -0,0 +1,6 @@
1
+ import { register as applyTextStyle } from './applyTextStyle.js';
2
+ import { register as applyParagraphStyle } from './applyParagraphStyle.js';
3
+ export function registerFormattingTools(server) {
4
+ applyTextStyle(server);
5
+ applyParagraphStyle(server);
6
+ }
@@ -0,0 +1,38 @@
1
+ // Core read/write
2
+ import { register as readGoogleDoc } from './readGoogleDoc.js';
3
+ import { register as listDocumentTabs } from './listDocumentTabs.js';
4
+ import { register as renameTab } from './renameTab.js';
5
+ import { register as addTab } from './addTab.js';
6
+ import { register as appendToGoogleDoc } from './appendToGoogleDoc.js';
7
+ import { register as insertText } from './insertText.js';
8
+ import { register as deleteRange } from './deleteRange.js';
9
+ import { register as modifyText } from './modifyText.js';
10
+ import { register as findAndReplace } from './findAndReplace.js';
11
+ // Structure
12
+ import { register as insertTable } from './insertTable.js';
13
+ import { register as insertTableWithData } from './insertTableWithData.js';
14
+ import { register as insertPageBreak } from './insertPageBreak.js';
15
+ import { register as insertImage } from './insertImage.js';
16
+ // Sub-domains
17
+ import { registerCommentTools } from './comments/index.js';
18
+ import { registerFormattingTools } from './formatting/index.js';
19
+ export function registerDocsTools(server) {
20
+ // Core read/write
21
+ readGoogleDoc(server);
22
+ listDocumentTabs(server);
23
+ renameTab(server);
24
+ addTab(server);
25
+ appendToGoogleDoc(server);
26
+ insertText(server);
27
+ deleteRange(server);
28
+ modifyText(server);
29
+ findAndReplace(server);
30
+ // Structure
31
+ insertTable(server);
32
+ insertTableWithData(server);
33
+ insertPageBreak(server);
34
+ insertImage(server);
35
+ // Sub-domains
36
+ registerFormattingTools(server);
37
+ registerCommentTools(server);
38
+ }
@@ -0,0 +1,122 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient, getDriveClient, getScriptClient } 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: 'insertImage',
9
+ description: 'Inserts an inline image into a Google Document. Provide either a publicly accessible URL or a local file path. Local files are automatically uploaded to Google Drive before insertion.',
10
+ parameters: DocumentIdParameter.extend({
11
+ imageUrl: z
12
+ .string()
13
+ .url()
14
+ .optional()
15
+ .describe('Publicly accessible URL to the image (http:// or https://).'),
16
+ localImagePath: z
17
+ .string()
18
+ .optional()
19
+ .describe('Absolute path to a local image file (supports .jpg, .jpeg, .png, .gif, .bmp, .webp, .svg). The file will be uploaded to Google Drive.'),
20
+ index: z
21
+ .number()
22
+ .int()
23
+ .min(1)
24
+ .describe("1-based character index in the document body where the image should be inserted. Use readDocument with format='json' to inspect indices."),
25
+ width: z.number().min(1).optional().describe('Width of the image in points.'),
26
+ height: z.number().min(1).optional().describe('Height of the image in points.'),
27
+ tabId: z
28
+ .string()
29
+ .optional()
30
+ .describe('The ID of the specific tab to insert into. Use listDocumentTabs to get tab IDs. If not specified, inserts into the first tab.'),
31
+ })
32
+ .refine((data) => data.imageUrl || data.localImagePath, {
33
+ message: 'Either imageUrl or localImagePath must be provided.',
34
+ })
35
+ .refine((data) => !(data.imageUrl && data.localImagePath), {
36
+ message: 'Provide only one of imageUrl or localImagePath, not both.',
37
+ }),
38
+ execute: async (args, { log }) => {
39
+ const docs = await getDocsClient();
40
+ const appsScriptDeploymentId = process.env.APPS_SCRIPT_DEPLOYMENT_ID;
41
+ try {
42
+ if (args.tabId) {
43
+ const docInfo = await docs.documents.get({
44
+ documentId: args.documentId,
45
+ includeTabsContent: true,
46
+ fields: 'tabs(tabProperties,documentTab)',
47
+ });
48
+ const targetTab = GDocsHelpers.findTabById(docInfo.data, args.tabId);
49
+ if (!targetTab) {
50
+ throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
51
+ }
52
+ if (!targetTab.documentTab) {
53
+ throw new UserError(`Tab "${args.tabId}" does not have content (may not be a document tab).`);
54
+ }
55
+ }
56
+ // --- Apps Script path: local files when APPS_SCRIPT_DEPLOYMENT_ID is set ---
57
+ if (args.localImagePath && appsScriptDeploymentId) {
58
+ const drive = await getDriveClient();
59
+ const scriptClient = await getScriptClient();
60
+ log.info(`[AppsScript] Uploading ${args.localImagePath} to Drive (no public sharing)`);
61
+ let parentFolderId;
62
+ try {
63
+ const docInfo = await drive.files.get({
64
+ fileId: args.documentId,
65
+ fields: 'parents',
66
+ supportsAllDrives: true,
67
+ });
68
+ if (docInfo.data.parents && docInfo.data.parents.length > 0) {
69
+ parentFolderId = docInfo.data.parents[0];
70
+ }
71
+ }
72
+ catch (folderError) {
73
+ log.warn(`Could not determine document's parent folder, using Drive root: ${folderError}`);
74
+ }
75
+ const driveFileId = await GDocsHelpers.uploadImageToDrive(drive, args.localImagePath, parentFolderId, true // skipPublicSharing
76
+ );
77
+ log.info(`[AppsScript] Inserting image via marker at index ${args.index} (fileId: ${driveFileId})`);
78
+ await GDocsHelpers.insertImageViaAppsScript(docs, scriptClient, appsScriptDeploymentId, args.documentId, driveFileId, args.index, args.tabId);
79
+ return `Successfully inserted local image at index ${args.index} via Apps Script${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
80
+ }
81
+ // --- Standard path: public URL insertion via Docs API ---
82
+ let resolvedUrl;
83
+ if (args.localImagePath) {
84
+ const drive = await getDriveClient();
85
+ log.info(`Uploading local image ${args.localImagePath} and inserting at index ${args.index} in doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
86
+ let parentFolderId;
87
+ try {
88
+ const docInfo = await drive.files.get({
89
+ fileId: args.documentId,
90
+ fields: 'parents',
91
+ supportsAllDrives: true,
92
+ });
93
+ if (docInfo.data.parents && docInfo.data.parents.length > 0) {
94
+ parentFolderId = docInfo.data.parents[0];
95
+ }
96
+ }
97
+ catch (folderError) {
98
+ log.warn(`Could not determine document's parent folder, using Drive root: ${folderError}`);
99
+ }
100
+ resolvedUrl = await GDocsHelpers.uploadImageToDrive(drive, args.localImagePath, parentFolderId);
101
+ log.info(`Image uploaded successfully, URL: ${resolvedUrl}`);
102
+ }
103
+ else {
104
+ resolvedUrl = args.imageUrl;
105
+ log.info(`Inserting image from URL ${resolvedUrl} at index ${args.index} in doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
106
+ }
107
+ await GDocsHelpers.insertInlineImage(docs, args.documentId, resolvedUrl, args.index, args.width, args.height, args.tabId);
108
+ let sizeInfo = '';
109
+ if (args.width && args.height) {
110
+ sizeInfo = ` with size ${args.width}x${args.height}pt`;
111
+ }
112
+ return `Successfully inserted image at index ${args.index}${sizeInfo}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
113
+ }
114
+ catch (error) {
115
+ log.error(`Error inserting image in doc ${args.documentId}: ${error.message || error}`);
116
+ if (error instanceof UserError)
117
+ throw error;
118
+ throw new UserError(`Failed to insert image: ${error.message || 'Unknown error'}`);
119
+ }
120
+ },
121
+ });
122
+ }
@@ -0,0 +1,58 @@
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: 'insertPageBreak',
9
+ description: 'Inserts a page break at a character index in the document.',
10
+ parameters: DocumentIdParameter.extend({
11
+ index: z
12
+ .number()
13
+ .int()
14
+ .min(1)
15
+ .describe("1-based character index within the document body. Use readDocument with format='json' to inspect indices."),
16
+ tabId: z
17
+ .string()
18
+ .optional()
19
+ .describe('The ID of the specific tab to insert into. Use listDocumentTabs to get tab IDs. If not specified, inserts into the first tab.'),
20
+ }),
21
+ execute: async (args, { log }) => {
22
+ const docs = await getDocsClient();
23
+ log.info(`Inserting page break in doc ${args.documentId} at index ${args.index}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
24
+ try {
25
+ // If tabId is specified, verify the tab exists
26
+ if (args.tabId) {
27
+ const docInfo = await docs.documents.get({
28
+ documentId: args.documentId,
29
+ includeTabsContent: true,
30
+ fields: 'tabs(tabProperties,documentTab)',
31
+ });
32
+ const targetTab = GDocsHelpers.findTabById(docInfo.data, args.tabId);
33
+ if (!targetTab) {
34
+ throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
35
+ }
36
+ if (!targetTab.documentTab) {
37
+ throw new UserError(`Tab "${args.tabId}" does not have content (may not be a document tab).`);
38
+ }
39
+ }
40
+ const location = { index: args.index };
41
+ if (args.tabId) {
42
+ location.tabId = args.tabId;
43
+ }
44
+ const request = {
45
+ insertPageBreak: { location },
46
+ };
47
+ await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [request]);
48
+ return `Successfully inserted page break at index ${args.index}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
49
+ }
50
+ catch (error) {
51
+ log.error(`Error inserting page break in doc ${args.documentId}: ${error.message || error}`);
52
+ if (error instanceof UserError)
53
+ throw error;
54
+ throw new UserError(`Failed to insert page break: ${error.message || 'Unknown error'}`);
55
+ }
56
+ },
57
+ });
58
+ }
@@ -0,0 +1,53 @@
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 docInfo = await docs.documents.get({
30
+ documentId: args.documentId,
31
+ includeTabsContent: true,
32
+ fields: 'tabs(tabProperties,documentTab)',
33
+ });
34
+ const targetTab = GDocsHelpers.findTabById(docInfo.data, args.tabId);
35
+ if (!targetTab) {
36
+ throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
37
+ }
38
+ if (!targetTab.documentTab) {
39
+ throw new UserError(`Tab "${args.tabId}" does not have content (may not be a document tab).`);
40
+ }
41
+ }
42
+ await GDocsHelpers.createTable(docs, args.documentId, args.rows, args.columns, args.index, args.tabId);
43
+ return `Successfully inserted a ${args.rows}x${args.columns} table at index ${args.index}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
44
+ }
45
+ catch (error) {
46
+ log.error(`Error inserting table in doc ${args.documentId}: ${error.message || error}`);
47
+ if (error instanceof UserError)
48
+ throw error;
49
+ throw new UserError(`Failed to insert table: ${error.message || 'Unknown error'}`);
50
+ }
51
+ },
52
+ });
53
+ }
@@ -0,0 +1,135 @@
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()))
79
+ .min(1)
80
+ .describe('A 2D array of strings representing the table contents. Each inner array is one row. ' +
81
+ 'Example: [["Name", "Age"], ["Alice", "30"], ["Bob", "25"]]'),
82
+ index: z
83
+ .number()
84
+ .int()
85
+ .min(1)
86
+ .describe('1-based character index within the document body where the table should be inserted. ' +
87
+ "Use readDocument with format='json' to inspect indices."),
88
+ hasHeaderRow: z
89
+ .boolean()
90
+ .optional()
91
+ .default(false)
92
+ .describe('If true, the first row is treated as a header and its text will be bolded.'),
93
+ tabId: z
94
+ .string()
95
+ .optional()
96
+ .describe('The ID of the specific tab to insert into. Use listDocumentTabs to get tab IDs. ' +
97
+ 'If not specified, inserts into the first tab.'),
98
+ }),
99
+ execute: async (args, { log }) => {
100
+ const docs = await getDocsClient();
101
+ const numRows = args.data.length;
102
+ const numCols = args.data.reduce((max, row) => Math.max(max, row.length), 0);
103
+ log.info(`Inserting ${numRows}x${numCols} table with data in doc ${args.documentId} at index ${args.index}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
104
+ try {
105
+ // Validate tab if specified (same pattern as insertTable.ts)
106
+ if (args.tabId) {
107
+ const docInfo = await docs.documents.get({
108
+ documentId: args.documentId,
109
+ includeTabsContent: true,
110
+ fields: 'tabs(tabProperties,documentTab)',
111
+ });
112
+ const targetTab = GDocsHelpers.findTabById(docInfo.data, args.tabId);
113
+ if (!targetTab) {
114
+ throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
115
+ }
116
+ if (!targetTab.documentTab) {
117
+ throw new UserError(`Tab "${args.tabId}" does not have content (may not be a document tab).`);
118
+ }
119
+ }
120
+ const requests = buildInsertTableWithDataRequests(args.data, args.index, args.hasHeaderRow ?? false, args.tabId);
121
+ const metadata = await GDocsHelpers.executeBatchUpdateWithSplitting(docs, args.documentId, requests, log);
122
+ return (`Successfully inserted a ${numRows}x${numCols} table with data at index ${args.index}` +
123
+ `${args.tabId ? ` in tab ${args.tabId}` : ''}. ` +
124
+ `${args.hasHeaderRow ? 'Header row bolded. ' : ''}` +
125
+ `(${metadata.totalRequests} requests in ${metadata.totalApiCalls} API calls, ${metadata.totalElapsedMs}ms)`);
126
+ }
127
+ catch (error) {
128
+ log.error(`Error inserting table with data in doc ${args.documentId}: ${error.message || error}`);
129
+ if (error instanceof UserError)
130
+ throw error;
131
+ throw new UserError(`Failed to insert table with data: ${error.message || 'Unknown error'}`);
132
+ }
133
+ },
134
+ });
135
+ }
@@ -0,0 +1,61 @@
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
+ // For tab-specific inserts, we need to verify the tab exists first
28
+ const docInfo = await docs.documents.get({
29
+ documentId: args.documentId,
30
+ includeTabsContent: true,
31
+ fields: 'tabs(tabProperties,documentTab)',
32
+ });
33
+ const targetTab = GDocsHelpers.findTabById(docInfo.data, args.tabId);
34
+ if (!targetTab) {
35
+ throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
36
+ }
37
+ if (!targetTab.documentTab) {
38
+ throw new UserError(`Tab "${args.tabId}" does not have content (may not be a document tab).`);
39
+ }
40
+ // Insert with tabId
41
+ const location = { index: args.index, tabId: args.tabId };
42
+ const request = {
43
+ insertText: { location, text: args.text },
44
+ };
45
+ await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [request]);
46
+ }
47
+ else {
48
+ // Use existing helper for backward compatibility
49
+ await GDocsHelpers.insertText(docs, args.documentId, args.text, args.index);
50
+ }
51
+ return `Successfully inserted text at index ${args.index}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
52
+ }
53
+ catch (error) {
54
+ log.error(`Error inserting text in doc ${args.documentId}: ${error.message || error}`);
55
+ if (error instanceof UserError)
56
+ throw error;
57
+ throw new UserError(`Failed to insert text: ${error.message || 'Unknown error'}`);
58
+ }
59
+ },
60
+ });
61
+ }