@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.
- package/LICENSE +7 -0
- package/README.md +567 -0
- package/dist/auth.js +327 -0
- package/dist/cachedToolsList.js +38 -0
- package/dist/clients.js +180 -0
- package/dist/config.js +9 -0
- package/dist/downloadProxy.js +82 -0
- package/dist/driveQueryUtils.js +7 -0
- package/dist/firestoreTokenStorage.js +32 -0
- package/dist/googleDocsApiHelpers.js +1257 -0
- package/dist/googleSheetsApiHelpers.js +849 -0
- package/dist/index.js +240 -0
- package/dist/landingPage.js +103 -0
- package/dist/logger.js +58 -0
- package/dist/markdown-transformer/docsToMarkdown.js +259 -0
- package/dist/markdown-transformer/index.js +128 -0
- package/dist/markdown-transformer/markdownToDocs.js +834 -0
- package/dist/remoteWrapper.js +72 -0
- package/dist/tools/calendar/createEvent.js +89 -0
- package/dist/tools/calendar/deleteEvent.js +49 -0
- package/dist/tools/calendar/helpers.js +19 -0
- package/dist/tools/calendar/index.js +12 -0
- package/dist/tools/calendar/listEvents.js +84 -0
- package/dist/tools/calendar/quickAddEvent.js +55 -0
- package/dist/tools/calendar/updateEvent.js +84 -0
- package/dist/tools/docs/addTab.js +84 -0
- package/dist/tools/docs/appendTableRows.js +89 -0
- package/dist/tools/docs/appendToGoogleDoc.js +85 -0
- package/dist/tools/docs/cloneTable.js +159 -0
- package/dist/tools/docs/comments/addComment.js +83 -0
- package/dist/tools/docs/comments/deleteComment.js +30 -0
- package/dist/tools/docs/comments/getComment.js +45 -0
- package/dist/tools/docs/comments/index.js +14 -0
- package/dist/tools/docs/comments/listComments.js +43 -0
- package/dist/tools/docs/comments/replyToComment.js +35 -0
- package/dist/tools/docs/comments/resolveComment.js +55 -0
- package/dist/tools/docs/deleteRange.js +61 -0
- package/dist/tools/docs/deleteTableRows.js +62 -0
- package/dist/tools/docs/findAndReplace.js +54 -0
- package/dist/tools/docs/findElement.js +43 -0
- package/dist/tools/docs/findSectionsByHeading.js +46 -0
- package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
- package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
- package/dist/tools/docs/formatting/batchApplyTextStyle.js +86 -0
- package/dist/tools/docs/formatting/index.js +16 -0
- package/dist/tools/docs/formatting/updateTableBorders.js +81 -0
- package/dist/tools/docs/formatting/updateTableCellStyle.js +85 -0
- package/dist/tools/docs/formatting/updateTableColumnWidth.js +51 -0
- package/dist/tools/docs/formatting/updateTableRowStyle.js +74 -0
- package/dist/tools/docs/getTableStructure.js +48 -0
- package/dist/tools/docs/index.js +66 -0
- package/dist/tools/docs/insertDateChip.js +82 -0
- package/dist/tools/docs/insertImage.js +112 -0
- package/dist/tools/docs/insertPageBreak.js +47 -0
- package/dist/tools/docs/insertPerson.js +53 -0
- package/dist/tools/docs/insertRichLink.js +58 -0
- package/dist/tools/docs/insertSectionBreak.js +60 -0
- package/dist/tools/docs/insertTable.js +42 -0
- package/dist/tools/docs/insertTableWithData.js +125 -0
- package/dist/tools/docs/insertText.js +49 -0
- package/dist/tools/docs/listDocumentTables.js +47 -0
- package/dist/tools/docs/listDocumentTabs.js +59 -0
- package/dist/tools/docs/listSmartChips.js +41 -0
- package/dist/tools/docs/modifyText.js +147 -0
- package/dist/tools/docs/readGoogleDoc.js +164 -0
- package/dist/tools/docs/renameTab.js +47 -0
- package/dist/tools/docs/replaceTableRowData.js +55 -0
- package/dist/tools/docs/smartChipHelpers.js +71 -0
- package/dist/tools/docs/structureHelpers.js +250 -0
- package/dist/tools/docs/tabFieldMasks.js +48 -0
- package/dist/tools/docs/tableRowDataHelpers.js +53 -0
- package/dist/tools/docs/updateSectionStyle.js +148 -0
- package/dist/tools/drive/copyFile.js +63 -0
- package/dist/tools/drive/createDocument.js +105 -0
- package/dist/tools/drive/createFolder.js +48 -0
- package/dist/tools/drive/createFromTemplate.js +82 -0
- package/dist/tools/drive/deleteFile.js +72 -0
- package/dist/tools/drive/downloadFile.js +266 -0
- package/dist/tools/drive/getDocumentInfo.js +48 -0
- package/dist/tools/drive/getFolderInfo.js +48 -0
- package/dist/tools/drive/index.js +34 -0
- package/dist/tools/drive/listDriveFiles.js +129 -0
- package/dist/tools/drive/listFolderContents.js +83 -0
- package/dist/tools/drive/listGoogleDocs.js +70 -0
- package/dist/tools/drive/moveFile.js +54 -0
- package/dist/tools/drive/renameFile.js +39 -0
- package/dist/tools/drive/savePathGuard.js +86 -0
- package/dist/tools/drive/searchDriveFiles.js +148 -0
- package/dist/tools/drive/searchGoogleDocs.js +77 -0
- package/dist/tools/drive/setFilePermission.js +68 -0
- package/dist/tools/gmail/createDraft.js +58 -0
- package/dist/tools/gmail/deleteDraft.js +37 -0
- package/dist/tools/gmail/getDraft.js +52 -0
- package/dist/tools/gmail/getMessage.js +92 -0
- package/dist/tools/gmail/helpers.js +113 -0
- package/dist/tools/gmail/index.js +28 -0
- package/dist/tools/gmail/listDrafts.js +74 -0
- package/dist/tools/gmail/listLabels.js +31 -0
- package/dist/tools/gmail/listMessages.js +87 -0
- package/dist/tools/gmail/modifyMessageLabels.js +54 -0
- package/dist/tools/gmail/sendDraft.js +42 -0
- package/dist/tools/gmail/sendEmail.js +56 -0
- package/dist/tools/gmail/trashMessage.js +37 -0
- package/dist/tools/gmail/triageInbox.js +147 -0
- package/dist/tools/gmail/updateDraft.js +60 -0
- package/dist/tools/index.js +64 -0
- package/dist/tools/script/appsScriptShared.js +96 -0
- package/dist/tools/script/createAppsScriptProject.js +74 -0
- package/dist/tools/script/getAppsScriptContent.js +56 -0
- package/dist/tools/script/index.js +14 -0
- package/dist/tools/script/updateAppsScriptContent.js +56 -0
- package/dist/tools/sheets/addConditionalFormatting.js +143 -0
- package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
- package/dist/tools/sheets/appendSpreadsheetRows.js +44 -0
- package/dist/tools/sheets/appendTableRows.js +51 -0
- package/dist/tools/sheets/autoResizeColumns.js +67 -0
- package/dist/tools/sheets/autoResizeRows.js +63 -0
- package/dist/tools/sheets/batchWrite.js +61 -0
- package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
- package/dist/tools/sheets/comments/commentAnchor.js +95 -0
- package/dist/tools/sheets/comments/createSheetsCellNote.js +33 -0
- package/dist/tools/sheets/comments/createSheetsComment.js +168 -0
- package/dist/tools/sheets/comments/deleteSheetsComment.js +32 -0
- package/dist/tools/sheets/comments/getSheetsComment.js +51 -0
- package/dist/tools/sheets/comments/index.js +16 -0
- package/dist/tools/sheets/comments/listSheetsComments.js +177 -0
- package/dist/tools/sheets/comments/replyToSheetsComment.js +37 -0
- package/dist/tools/sheets/comments/resolveSheetsComment.js +53 -0
- package/dist/tools/sheets/copyFormatting.js +59 -0
- package/dist/tools/sheets/copySheetTo.js +36 -0
- package/dist/tools/sheets/createSpreadsheet.js +72 -0
- package/dist/tools/sheets/createTable.js +120 -0
- package/dist/tools/sheets/deleteChart.js +41 -0
- package/dist/tools/sheets/deleteConditionalFormatting.js +46 -0
- package/dist/tools/sheets/deleteSheet.js +43 -0
- package/dist/tools/sheets/deleteTable.js +56 -0
- package/dist/tools/sheets/duplicateSheet.js +53 -0
- package/dist/tools/sheets/formatCells.js +122 -0
- package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
- package/dist/tools/sheets/getConditionalFormatting.js +98 -0
- package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
- package/dist/tools/sheets/getTable.js +48 -0
- package/dist/tools/sheets/groupRows.js +62 -0
- package/dist/tools/sheets/index.js +84 -0
- package/dist/tools/sheets/insertChart.js +225 -0
- package/dist/tools/sheets/listGoogleSheets.js +66 -0
- package/dist/tools/sheets/listTables.js +55 -0
- package/dist/tools/sheets/protectRange.js +59 -0
- package/dist/tools/sheets/readCellFormat.js +143 -0
- package/dist/tools/sheets/readSpreadsheet.js +36 -0
- package/dist/tools/sheets/renameSheet.js +48 -0
- package/dist/tools/sheets/setCellBorders.js +94 -0
- package/dist/tools/sheets/setColumnWidths.js +43 -0
- package/dist/tools/sheets/setDropdownValidation.js +51 -0
- package/dist/tools/sheets/setRowHeights.js +63 -0
- package/dist/tools/sheets/ungroupAllRows.js +66 -0
- package/dist/tools/sheets/updateTableRange.js +51 -0
- package/dist/tools/sheets/writeSpreadsheet.js +45 -0
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +95 -0
- package/dist/tools/utils/index.js +8 -0
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +161 -0
- package/dist/tools/utils/replaceRangeWithMarkdown.js +72 -0
- package/dist/types.js +208 -0
- package/dist/upstreamAuth.js +62 -0
- package/package.json +51 -0
|
@@ -0,0 +1,62 @@
|
|
|
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 { getTableById } from './structureHelpers.js';
|
|
7
|
+
import { TABLE_CONTENT_BASIC_BODY_FIELDS, buildDocumentGetFields } from './tabFieldMasks.js';
|
|
8
|
+
export function register(server) {
|
|
9
|
+
server.addTool({
|
|
10
|
+
name: 'deleteTableRows',
|
|
11
|
+
description: 'Deletes one or more rows from an existing Google Docs table without replacing the whole document.',
|
|
12
|
+
parameters: DocumentIdParameter.extend({
|
|
13
|
+
tableId: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(1)
|
|
16
|
+
.describe('The MCP table ID returned by listDocumentTables, for example "table:body:0".'),
|
|
17
|
+
rowStart: z.number().int().min(0).describe('Zero-based starting row index to delete.'),
|
|
18
|
+
rowCount: z.number().int().min(1).describe('Number of rows to delete.'),
|
|
19
|
+
tabId: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('The ID of the specific tab containing the table. If not specified, uses the first tab or legacy document body.'),
|
|
23
|
+
}),
|
|
24
|
+
execute: async (args, { log }) => {
|
|
25
|
+
const docs = await getDocsClient();
|
|
26
|
+
log.info(`Deleting ${args.rowCount} row(s) from ${args.tableId} in doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
|
|
27
|
+
try {
|
|
28
|
+
const res = await docs.documents.get({
|
|
29
|
+
documentId: args.documentId,
|
|
30
|
+
...(args.tabId && { includeTabsContent: true }),
|
|
31
|
+
fields: buildDocumentGetFields(TABLE_CONTENT_BASIC_BODY_FIELDS, args.tabId),
|
|
32
|
+
});
|
|
33
|
+
const table = getTableById(res.data, args.tableId, args.tabId);
|
|
34
|
+
if (!table) {
|
|
35
|
+
throw new UserError(`Table "${args.tableId}" not found in document.`);
|
|
36
|
+
}
|
|
37
|
+
if (table.startIndex == null) {
|
|
38
|
+
throw new UserError(`Table "${args.tableId}" does not expose a valid table start index.`);
|
|
39
|
+
}
|
|
40
|
+
if (args.rowStart + args.rowCount > table.rowCount) {
|
|
41
|
+
throw new UserError(`Requested rows ${args.rowStart}-${args.rowStart + args.rowCount - 1} exceed table ${args.tableId} row count ${table.rowCount}.`);
|
|
42
|
+
}
|
|
43
|
+
const requests = [];
|
|
44
|
+
for (let rowIndex = args.rowStart + args.rowCount - 1; rowIndex >= args.rowStart; rowIndex--) {
|
|
45
|
+
requests.push(GDocsHelpers.buildDeleteTableRowRequest(table.startIndex, rowIndex, args.tabId));
|
|
46
|
+
}
|
|
47
|
+
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, requests);
|
|
48
|
+
return `Successfully deleted ${args.rowCount} row(s) from table ${args.tableId}.`;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
log.error(`Error deleting rows from ${args.tableId} in doc ${args.documentId}: ${error.message || error}`);
|
|
52
|
+
if (error instanceof UserError)
|
|
53
|
+
throw error;
|
|
54
|
+
if (error.code === 404)
|
|
55
|
+
throw new UserError(`Document not found (ID: ${args.documentId}).`);
|
|
56
|
+
if (error.code === 403)
|
|
57
|
+
throw new UserError(`Permission denied for document (ID: ${args.documentId}).`);
|
|
58
|
+
throw new UserError(`Failed to delete table rows: ${error.message || 'Unknown error'}`);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
const FindAndReplaceParameters = DocumentIdParameter.extend({
|
|
7
|
+
findText: z.string().min(1).describe('The text to search for in the document.'),
|
|
8
|
+
replaceText: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe('The replacement text. Use an empty string to delete all occurrences.'),
|
|
11
|
+
matchCase: z
|
|
12
|
+
.boolean()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('Whether the search should be case-sensitive. Defaults to false.'),
|
|
15
|
+
tabId: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Scope replacement to a specific tab. If omitted, replaces across all tabs.'),
|
|
19
|
+
});
|
|
20
|
+
export function register(server) {
|
|
21
|
+
server.addTool({
|
|
22
|
+
name: 'findAndReplace',
|
|
23
|
+
description: 'Replaces all occurrences of a text string throughout the document (or a specific tab). ' +
|
|
24
|
+
'Returns the number of replacements made. Use an empty replaceText to delete all matches.',
|
|
25
|
+
parameters: FindAndReplaceParameters,
|
|
26
|
+
execute: async (args, { log }) => {
|
|
27
|
+
const docs = await getDocsClient();
|
|
28
|
+
log.info(`findAndReplace in doc ${args.documentId}: "${args.findText}" → "${args.replaceText}"` +
|
|
29
|
+
`${args.matchCase ? ' (case-sensitive)' : ''}` +
|
|
30
|
+
`${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
|
|
31
|
+
try {
|
|
32
|
+
const request = {
|
|
33
|
+
replaceAllText: {
|
|
34
|
+
containsText: {
|
|
35
|
+
text: args.findText,
|
|
36
|
+
matchCase: args.matchCase ?? false,
|
|
37
|
+
},
|
|
38
|
+
replaceText: args.replaceText,
|
|
39
|
+
...(args.tabId && { tabsCriteria: { tabIds: [args.tabId] } }),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const response = await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [request]);
|
|
43
|
+
const changed = response.replies?.[0]?.replaceAllText?.occurrencesChanged ?? 0;
|
|
44
|
+
return `Replaced ${changed} occurrence(s) of "${args.findText}" with "${args.replaceText}".`;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
log.error(`Error in findAndReplace for doc ${args.documentId}: ${error.message || error}`);
|
|
48
|
+
if (error instanceof UserError)
|
|
49
|
+
throw error;
|
|
50
|
+
throw new UserError(`Failed to find and replace: ${error.message || 'Unknown error'}`);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
const FindElementParameters = DocumentIdParameter.extend({
|
|
7
|
+
textQuery: z
|
|
8
|
+
.string()
|
|
9
|
+
.min(1)
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Exact text to locate. Returns one result per occurrence with its document index range.'),
|
|
12
|
+
elementType: z
|
|
13
|
+
.enum(['paragraph', 'table', 'list', 'image'])
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('List structural elements of this type. Only "paragraph" and "table" are supported; "list" and "image" are rejected.'),
|
|
16
|
+
});
|
|
17
|
+
export function register(server) {
|
|
18
|
+
server.addTool({
|
|
19
|
+
name: 'findElement',
|
|
20
|
+
description: 'Locates elements in a Google Document. With textQuery, returns the document index range (startIndex/endIndex) of every non-overlapping occurrence of the text — use each as a range for deleteRange or the text-styling tools, or its startIndex as an insertText location. With elementType "paragraph" or "table", lists those elements with their ranges and a text preview. Limitations: searches the first tab only (on multi-tab documents, tabs 2+ are not searched); paragraph listing covers top-level body paragraphs only, not paragraphs inside table cells (textQuery does search inside cells, but only one level deep — text in a table nested inside a table cell is not searched). At least one of textQuery or elementType is required.',
|
|
21
|
+
parameters: FindElementParameters,
|
|
22
|
+
execute: async (args, { log }) => {
|
|
23
|
+
const docs = await getDocsClient();
|
|
24
|
+
log.info(`findElement in doc ${args.documentId}: textQuery=${args.textQuery ?? 'none'}, elementType=${args.elementType ?? 'none'}`);
|
|
25
|
+
try {
|
|
26
|
+
const found = await GDocsHelpers.findElements(docs, args.documentId, {
|
|
27
|
+
textQuery: args.textQuery,
|
|
28
|
+
elementType: args.elementType,
|
|
29
|
+
});
|
|
30
|
+
if (found.length === 0) {
|
|
31
|
+
return `No matching elements found (textQuery=${args.textQuery ?? 'none'}, elementType=${args.elementType ?? 'none'}).`;
|
|
32
|
+
}
|
|
33
|
+
return JSON.stringify({ count: found.length, elements: found }, null, 2);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
log.error(`Error in findElement for doc ${args.documentId}: ${error.message || error}`);
|
|
37
|
+
if (error instanceof UserError)
|
|
38
|
+
throw error;
|
|
39
|
+
throw new UserError(`Failed to find elements: ${error.message || 'Unknown error'}`);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDocsClient } from '../../clients.js';
|
|
4
|
+
import { DocumentIdParameter } from '../../types.js';
|
|
5
|
+
import { findHeadings } from './structureHelpers.js';
|
|
6
|
+
import { HEADING_BODY_FIELDS, buildDocumentGetFields } from './tabFieldMasks.js';
|
|
7
|
+
export function register(server) {
|
|
8
|
+
server.addTool({
|
|
9
|
+
name: 'findSectionsByHeading',
|
|
10
|
+
description: 'Finds heading-based sections in a Google Document and reports heading ranges plus the first table that follows each heading.',
|
|
11
|
+
parameters: DocumentIdParameter.extend({
|
|
12
|
+
headings: z
|
|
13
|
+
.array(z.string().min(1))
|
|
14
|
+
.min(1)
|
|
15
|
+
.max(50)
|
|
16
|
+
.describe('List of exact heading texts to locate in the document.'),
|
|
17
|
+
tabId: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('The ID of the specific tab to inspect. If not specified, inspects the first tab or legacy document body.'),
|
|
21
|
+
}),
|
|
22
|
+
execute: async (args, { log }) => {
|
|
23
|
+
const docs = await getDocsClient();
|
|
24
|
+
log.info(`Finding sections by heading in ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}: ${args.headings.join(', ')}`);
|
|
25
|
+
try {
|
|
26
|
+
const res = await docs.documents.get({
|
|
27
|
+
documentId: args.documentId,
|
|
28
|
+
...(args.tabId && { includeTabsContent: true }),
|
|
29
|
+
fields: buildDocumentGetFields(HEADING_BODY_FIELDS, args.tabId),
|
|
30
|
+
});
|
|
31
|
+
const sections = findHeadings(res.data, args.headings, args.tabId);
|
|
32
|
+
return JSON.stringify({ sections }, null, 2);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
log.error(`Error finding sections by heading in doc ${args.documentId}: ${error.message || error}`);
|
|
36
|
+
if (error instanceof UserError)
|
|
37
|
+
throw error;
|
|
38
|
+
if (error.code === 404)
|
|
39
|
+
throw new UserError(`Document not found (ID: ${args.documentId}).`);
|
|
40
|
+
if (error.code === 403)
|
|
41
|
+
throw new UserError(`Permission denied for document (ID: ${args.documentId}).`);
|
|
42
|
+
throw new UserError(`Failed to find sections by heading: ${error.message || 'Unknown error'}`);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -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,86 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { getDocsClient } from '../../../clients.js';
|
|
3
|
+
import { BatchApplyTextStyleToolParameters } from '../../../types.js';
|
|
4
|
+
import * as GDocsHelpers from '../../../googleDocsApiHelpers.js';
|
|
5
|
+
// Mirrors MAX_BATCH_UPDATE_REQUESTS in googleDocsApiHelpers.ts — Google's own
|
|
6
|
+
// per-batchUpdate-call request ceiling, not an arbitrary choice.
|
|
7
|
+
const MAX_REQUESTS_PER_CALL = 50;
|
|
8
|
+
export function register(server) {
|
|
9
|
+
server.addTool({
|
|
10
|
+
name: 'batchApplyTextStyle',
|
|
11
|
+
description: 'Applies character-level formatting (bold, italic, color, font, etc.) to MANY ranges in a single tool ' +
|
|
12
|
+
'call, instead of one applyTextStyle call per range. Built for scripts that colour dozens or hundreds of ' +
|
|
13
|
+
'cues: do one readDocument (format="json") pass, compute every range from that one read, then pass them ' +
|
|
14
|
+
"all here as one operations[] array. Internally chunks into batches of up to 50 requests per Docs API " +
|
|
15
|
+
'call (Google\'s per-request-array limit), so 100+ operations still cost only 2-3 API calls instead of ' +
|
|
16
|
+
'100+ round trips.',
|
|
17
|
+
parameters: BatchApplyTextStyleToolParameters,
|
|
18
|
+
execute: async (args, { log }) => {
|
|
19
|
+
const docs = await getDocsClient();
|
|
20
|
+
log.info(`Batch-applying text style in doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}. Operations: ${args.operations.length}`);
|
|
21
|
+
const requests = [];
|
|
22
|
+
const skipped = [];
|
|
23
|
+
const fieldsSeen = new Set();
|
|
24
|
+
for (let i = 0; i < args.operations.length; i++) {
|
|
25
|
+
const op = args.operations[i];
|
|
26
|
+
let startIndex;
|
|
27
|
+
let endIndex;
|
|
28
|
+
try {
|
|
29
|
+
if ('textToFind' in op.target) {
|
|
30
|
+
const range = await GDocsHelpers.findTextRange(docs, args.documentId, op.target.textToFind, op.target.matchInstance, args.tabId);
|
|
31
|
+
if (!range) {
|
|
32
|
+
skipped.push({
|
|
33
|
+
index: i,
|
|
34
|
+
reason: `Could not find instance ${op.target.matchInstance} of text "${op.target.textToFind}".`,
|
|
35
|
+
});
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
startIndex = range.startIndex;
|
|
39
|
+
endIndex = range.endIndex;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
startIndex = op.target.startIndex;
|
|
43
|
+
endIndex = op.target.endIndex;
|
|
44
|
+
}
|
|
45
|
+
if (startIndex === undefined || endIndex === undefined || endIndex <= startIndex) {
|
|
46
|
+
skipped.push({ index: i, reason: 'Target range could not be determined.' });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const requestInfo = GDocsHelpers.buildUpdateTextStyleRequest(startIndex, endIndex, op.style, args.tabId);
|
|
50
|
+
if (!requestInfo) {
|
|
51
|
+
skipped.push({ index: i, reason: 'No valid text styling options were provided.' });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
requests.push(requestInfo.request);
|
|
55
|
+
requestInfo.fields.forEach((f) => fieldsSeen.add(f));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
skipped.push({ index: i, reason: error.message || 'Unknown error resolving target.' });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (requests.length === 0) {
|
|
62
|
+
throw new UserError(`No operations could be applied. Skipped: ${JSON.stringify(skipped)}`);
|
|
63
|
+
}
|
|
64
|
+
let apiCalls = 0;
|
|
65
|
+
const totalChunks = Math.ceil(requests.length / MAX_REQUESTS_PER_CALL);
|
|
66
|
+
try {
|
|
67
|
+
for (let i = 0; i < requests.length; i += MAX_REQUESTS_PER_CALL) {
|
|
68
|
+
const chunk = requests.slice(i, i + MAX_REQUESTS_PER_CALL);
|
|
69
|
+
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, chunk);
|
|
70
|
+
apiCalls++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new UserError(`Batch failed after ${apiCalls} of ${totalChunks} chunk(s) succeeded ` +
|
|
75
|
+
`(${Math.min(apiCalls * MAX_REQUESTS_PER_CALL, requests.length)} of ${requests.length} operations ` +
|
|
76
|
+
`applied before the error). Error: ${error.message || error}`);
|
|
77
|
+
}
|
|
78
|
+
const summary = `Applied ${requests.length}/${args.operations.length} text style operation(s) ` +
|
|
79
|
+
`(${Array.from(fieldsSeen).join(', ')}) to doc ${args.documentId} in ${apiCalls} Docs API call(s).`;
|
|
80
|
+
if (skipped.length > 0) {
|
|
81
|
+
return `${summary} Skipped ${skipped.length}: ${JSON.stringify(skipped)}`;
|
|
82
|
+
}
|
|
83
|
+
return summary;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { register as applyTextStyle } from './applyTextStyle.js';
|
|
2
|
+
import { register as batchApplyTextStyle } from './batchApplyTextStyle.js';
|
|
3
|
+
import { register as applyParagraphStyle } from './applyParagraphStyle.js';
|
|
4
|
+
import { register as updateTableCellStyle } from './updateTableCellStyle.js';
|
|
5
|
+
import { register as updateTableBorders } from './updateTableBorders.js';
|
|
6
|
+
import { register as updateTableColumnWidth } from './updateTableColumnWidth.js';
|
|
7
|
+
import { register as updateTableRowStyle } from './updateTableRowStyle.js';
|
|
8
|
+
export function registerFormattingTools(server) {
|
|
9
|
+
applyTextStyle(server);
|
|
10
|
+
batchApplyTextStyle(server);
|
|
11
|
+
applyParagraphStyle(server);
|
|
12
|
+
updateTableCellStyle(server);
|
|
13
|
+
updateTableBorders(server);
|
|
14
|
+
updateTableColumnWidth(server);
|
|
15
|
+
updateTableRowStyle(server);
|
|
16
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDocsClient } from '../../../clients.js';
|
|
4
|
+
import { DocumentIdParameter, validateHexColor, hexToRgbColor } from '../../../types.js';
|
|
5
|
+
import { getTableById } from '../structureHelpers.js';
|
|
6
|
+
import * as GDocsHelpers from '../../../googleDocsApiHelpers.js';
|
|
7
|
+
import { TABLE_INDEX_BODY_FIELDS, buildDocumentGetFields } from '../tabFieldMasks.js';
|
|
8
|
+
const createBorderSideSchema = () => z.strictObject({
|
|
9
|
+
color: z
|
|
10
|
+
.string()
|
|
11
|
+
.refine(validateHexColor, { message: 'Invalid hex color format (e.g., #000000).' })
|
|
12
|
+
.optional(),
|
|
13
|
+
widthPt: z.number().min(0).optional(),
|
|
14
|
+
dashStyle: z.enum(['SOLID', 'DASHED', 'DOTTED']).optional(),
|
|
15
|
+
});
|
|
16
|
+
export function register(server) {
|
|
17
|
+
server.addTool({
|
|
18
|
+
name: 'updateTableBorders',
|
|
19
|
+
description: 'Applies table border styles to a Google Docs table range. Supports top/bottom/left/right borders across a cell range.',
|
|
20
|
+
parameters: DocumentIdParameter.extend({
|
|
21
|
+
tableId: z.string().min(1).describe('The MCP table ID returned by listDocumentTables.'),
|
|
22
|
+
rowStart: z.number().int().min(0).describe('Zero-based starting row index.'),
|
|
23
|
+
rowEnd: z.number().int().min(0).describe('Zero-based ending row index (inclusive).'),
|
|
24
|
+
columnStart: z.number().int().min(0).describe('Zero-based starting column index.'),
|
|
25
|
+
columnEnd: z.number().int().min(0).describe('Zero-based ending column index (inclusive).'),
|
|
26
|
+
top: createBorderSideSchema().optional(),
|
|
27
|
+
bottom: createBorderSideSchema().optional(),
|
|
28
|
+
left: createBorderSideSchema().optional(),
|
|
29
|
+
right: createBorderSideSchema().optional(),
|
|
30
|
+
tabId: z.string().optional().describe('Optional target tab ID.'),
|
|
31
|
+
})
|
|
32
|
+
.refine((data) => data.rowEnd >= data.rowStart, {
|
|
33
|
+
message: 'rowEnd must be greater than or equal to rowStart',
|
|
34
|
+
path: ['rowEnd'],
|
|
35
|
+
})
|
|
36
|
+
.refine((data) => data.columnEnd >= data.columnStart, {
|
|
37
|
+
message: 'columnEnd must be greater than or equal to columnStart',
|
|
38
|
+
path: ['columnEnd'],
|
|
39
|
+
}),
|
|
40
|
+
execute: async (args, { log }) => {
|
|
41
|
+
const docs = await getDocsClient();
|
|
42
|
+
log.info(`Updating table borders in ${args.tableId} for doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
|
|
43
|
+
try {
|
|
44
|
+
const res = await docs.documents.get({
|
|
45
|
+
documentId: args.documentId,
|
|
46
|
+
...(args.tabId && { includeTabsContent: true }),
|
|
47
|
+
fields: buildDocumentGetFields(TABLE_INDEX_BODY_FIELDS, args.tabId),
|
|
48
|
+
});
|
|
49
|
+
const table = getTableById(res.data, args.tableId, args.tabId);
|
|
50
|
+
if (!table)
|
|
51
|
+
throw new UserError(`Table "${args.tableId}" not found in document.`);
|
|
52
|
+
if (table.startIndex == null) {
|
|
53
|
+
throw new UserError(`Table "${args.tableId}" does not expose a valid table start index.`);
|
|
54
|
+
}
|
|
55
|
+
const defaultColor = hexToRgbColor('#000000');
|
|
56
|
+
const makeBorder = (side) => side
|
|
57
|
+
? GDocsHelpers.buildTableBorder(hexToRgbColor(side.color ?? '#000000') ?? defaultColor, side.widthPt ?? 1, side.dashStyle ?? 'SOLID')
|
|
58
|
+
: undefined;
|
|
59
|
+
const requestInfo = GDocsHelpers.buildTableCellStyleRequest(table.startIndex, args.rowStart, args.columnStart, {
|
|
60
|
+
rowSpan: args.rowEnd - args.rowStart + 1,
|
|
61
|
+
columnSpan: args.columnEnd - args.columnStart + 1,
|
|
62
|
+
borderTop: makeBorder(args.top),
|
|
63
|
+
borderBottom: makeBorder(args.bottom),
|
|
64
|
+
borderLeft: makeBorder(args.left),
|
|
65
|
+
borderRight: makeBorder(args.right),
|
|
66
|
+
}, args.tabId);
|
|
67
|
+
if (!requestInfo) {
|
|
68
|
+
throw new UserError('No border style options were provided.');
|
|
69
|
+
}
|
|
70
|
+
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [requestInfo.request]);
|
|
71
|
+
return `Successfully updated table borders (${requestInfo.fields.join(', ')}) for ${args.tableId}.`;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
log.error(`Error updating table borders for ${args.tableId} in doc ${args.documentId}: ${error.message || error}`);
|
|
75
|
+
if (error instanceof UserError)
|
|
76
|
+
throw error;
|
|
77
|
+
throw new UserError(`Failed to update table borders: ${error.message || 'Unknown error'}`);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDocsClient } from '../../../clients.js';
|
|
4
|
+
import { getTableById } from '../structureHelpers.js';
|
|
5
|
+
import * as GDocsHelpers from '../../../googleDocsApiHelpers.js';
|
|
6
|
+
import { DocumentIdParameter, validateHexColor, hexToRgbColor } from '../../../types.js';
|
|
7
|
+
import { TABLE_INDEX_BODY_FIELDS, buildDocumentGetFields } from '../tabFieldMasks.js';
|
|
8
|
+
const HexColor = z
|
|
9
|
+
.string()
|
|
10
|
+
.refine(validateHexColor, { message: 'Invalid hex color format (e.g., #D9E2F3).' });
|
|
11
|
+
const Alignment = z.enum(['TOP', 'MIDDLE', 'BOTTOM']);
|
|
12
|
+
export function register(server) {
|
|
13
|
+
server.addTool({
|
|
14
|
+
name: 'updateTableCellStyle',
|
|
15
|
+
description: 'Applies cell-level formatting to a Google Docs table range, including background color, alignment, padding, and borders.',
|
|
16
|
+
parameters: DocumentIdParameter.extend({
|
|
17
|
+
tableId: z.string().min(1).describe('The MCP table ID returned by listDocumentTables.'),
|
|
18
|
+
rowStart: z.number().int().min(0).describe('Zero-based starting row index.'),
|
|
19
|
+
rowEnd: z.number().int().min(0).describe('Zero-based ending row index (inclusive).'),
|
|
20
|
+
columnStart: z.number().int().min(0).describe('Zero-based starting column index.'),
|
|
21
|
+
columnEnd: z.number().int().min(0).describe('Zero-based ending column index (inclusive).'),
|
|
22
|
+
backgroundColor: HexColor.optional().describe('Cell background color, e.g. "#D9E2F3".'),
|
|
23
|
+
contentAlignment: Alignment.optional().describe('Vertical content alignment inside cells.'),
|
|
24
|
+
paddingTopPt: z.number().min(0).optional().describe('Top padding in points.'),
|
|
25
|
+
paddingBottomPt: z.number().min(0).optional().describe('Bottom padding in points.'),
|
|
26
|
+
paddingLeftPt: z.number().min(0).optional().describe('Left padding in points.'),
|
|
27
|
+
paddingRightPt: z.number().min(0).optional().describe('Right padding in points.'),
|
|
28
|
+
tabId: z.string().optional().describe('Optional target tab ID.'),
|
|
29
|
+
})
|
|
30
|
+
.refine((data) => data.rowEnd >= data.rowStart, {
|
|
31
|
+
message: 'rowEnd must be greater than or equal to rowStart',
|
|
32
|
+
path: ['rowEnd'],
|
|
33
|
+
})
|
|
34
|
+
.refine((data) => data.columnEnd >= data.columnStart, {
|
|
35
|
+
message: 'columnEnd must be greater than or equal to columnStart',
|
|
36
|
+
path: ['columnEnd'],
|
|
37
|
+
}),
|
|
38
|
+
execute: async (args, { log }) => {
|
|
39
|
+
const docs = await getDocsClient();
|
|
40
|
+
log.info(`Updating table cell style in ${args.tableId} for doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
|
|
41
|
+
try {
|
|
42
|
+
const res = await docs.documents.get({
|
|
43
|
+
documentId: args.documentId,
|
|
44
|
+
...(args.tabId && { includeTabsContent: true }),
|
|
45
|
+
fields: buildDocumentGetFields(TABLE_INDEX_BODY_FIELDS, args.tabId),
|
|
46
|
+
});
|
|
47
|
+
const table = getTableById(res.data, args.tableId, args.tabId);
|
|
48
|
+
if (!table)
|
|
49
|
+
throw new UserError(`Table "${args.tableId}" not found in document.`);
|
|
50
|
+
if (table.startIndex == null) {
|
|
51
|
+
throw new UserError(`Table "${args.tableId}" does not expose a valid table start index.`);
|
|
52
|
+
}
|
|
53
|
+
if (args.rowEnd >= table.rowCount) {
|
|
54
|
+
throw new UserError(`rowEnd ${args.rowEnd} exceeds table row count ${table.rowCount}.`);
|
|
55
|
+
}
|
|
56
|
+
if (args.columnEnd >= table.columnCount) {
|
|
57
|
+
throw new UserError(`columnEnd ${args.columnEnd} exceeds table column count ${table.columnCount}.`);
|
|
58
|
+
}
|
|
59
|
+
const requestInfo = GDocsHelpers.buildTableCellStyleRequest(table.startIndex, args.rowStart, args.columnStart, {
|
|
60
|
+
rowSpan: args.rowEnd - args.rowStart + 1,
|
|
61
|
+
columnSpan: args.columnEnd - args.columnStart + 1,
|
|
62
|
+
backgroundColor: args.backgroundColor
|
|
63
|
+
? (hexToRgbColor(args.backgroundColor) ?? undefined)
|
|
64
|
+
: undefined,
|
|
65
|
+
contentAlignment: args.contentAlignment,
|
|
66
|
+
paddingTopPt: args.paddingTopPt,
|
|
67
|
+
paddingBottomPt: args.paddingBottomPt,
|
|
68
|
+
paddingLeftPt: args.paddingLeftPt,
|
|
69
|
+
paddingRightPt: args.paddingRightPt,
|
|
70
|
+
}, args.tabId);
|
|
71
|
+
if (!requestInfo) {
|
|
72
|
+
throw new UserError('No table cell style options were provided.');
|
|
73
|
+
}
|
|
74
|
+
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [requestInfo.request]);
|
|
75
|
+
return `Successfully updated table cell style (${requestInfo.fields.join(', ')}) for ${args.tableId}.`;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
log.error(`Error updating table cell style for ${args.tableId} in doc ${args.documentId}: ${error.message || error}`);
|
|
79
|
+
if (error instanceof UserError)
|
|
80
|
+
throw error;
|
|
81
|
+
throw new UserError(`Failed to update table cell style: ${error.message || 'Unknown error'}`);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|