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.
- package/dist/auth.js +275 -0
- package/dist/cachedToolsList.js +52 -0
- package/dist/clients.js +97 -0
- package/dist/googleDocsApiHelpers.js +883 -0
- package/dist/googleSheetsApiHelpers.js +808 -0
- package/dist/helpers.js +210 -0
- package/dist/index.js +55 -0
- package/dist/logger.js +45 -0
- package/dist/markdown-transformer/docsToMarkdown.js +259 -0
- package/dist/markdown-transformer/index.js +126 -0
- package/dist/markdown-transformer/markdownToDocs.js +834 -0
- package/dist/tools/docs/addTab.js +92 -0
- package/dist/tools/docs/appendToGoogleDoc.js +81 -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 +72 -0
- package/dist/tools/docs/findAndReplace.js +54 -0
- package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
- package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
- package/dist/tools/docs/formatting/index.js +6 -0
- package/dist/tools/docs/index.js +38 -0
- package/dist/tools/docs/insertImage.js +122 -0
- package/dist/tools/docs/insertPageBreak.js +58 -0
- package/dist/tools/docs/insertTable.js +53 -0
- package/dist/tools/docs/insertTableWithData.js +135 -0
- package/dist/tools/docs/insertText.js +61 -0
- package/dist/tools/docs/listDocumentTabs.js +60 -0
- package/dist/tools/docs/modifyText.js +158 -0
- package/dist/tools/docs/readGoogleDoc.js +165 -0
- package/dist/tools/docs/renameTab.js +61 -0
- package/dist/tools/drive/copyFile.js +63 -0
- package/dist/tools/drive/createDocument.js +89 -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/getDocumentInfo.js +48 -0
- package/dist/tools/drive/getFolderInfo.js +48 -0
- package/dist/tools/drive/index.js +30 -0
- package/dist/tools/drive/listFolderContents.js +82 -0
- package/dist/tools/drive/listGoogleDocs.js +67 -0
- package/dist/tools/drive/listSharedDrives.js +52 -0
- package/dist/tools/drive/listSharedWithMe.js +97 -0
- package/dist/tools/drive/moveFile.js +54 -0
- package/dist/tools/drive/renameFile.js +39 -0
- package/dist/tools/drive/searchGoogleDocs.js +98 -0
- package/dist/tools/extras/index.js +7 -0
- package/dist/tools/extras/readFile.js +82 -0
- package/dist/tools/extras/searchFileContents.js +81 -0
- package/dist/tools/gmail/drafts.js +165 -0
- package/dist/tools/gmail/labels.js +103 -0
- package/dist/tools/gmail/messages.js +448 -0
- package/dist/tools/gmail/settings.js +528 -0
- package/dist/tools/gmail/threads.js +145 -0
- package/dist/tools/index.js +163 -0
- package/dist/tools/sheets/addConditionalFormatting.js +143 -0
- package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
- package/dist/tools/sheets/appendSpreadsheetRows.js +43 -0
- package/dist/tools/sheets/appendTableRows.js +50 -0
- package/dist/tools/sheets/autoResizeColumns.js +67 -0
- package/dist/tools/sheets/batchWrite.js +59 -0
- package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
- package/dist/tools/sheets/copyFormatting.js +59 -0
- package/dist/tools/sheets/createSpreadsheet.js +71 -0
- package/dist/tools/sheets/createTable.js +120 -0
- package/dist/tools/sheets/deleteChart.js +41 -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 +106 -0
- package/dist/tools/sheets/freezeRowsAndColumns.js +58 -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 +66 -0
- package/dist/tools/sheets/insertChart.js +225 -0
- package/dist/tools/sheets/listGoogleSheets.js +62 -0
- package/dist/tools/sheets/listTables.js +55 -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/setColumnWidths.js +43 -0
- package/dist/tools/sheets/setDropdownValidation.js +51 -0
- package/dist/tools/sheets/ungroupAllRows.js +66 -0
- package/dist/tools/sheets/updateTableRange.js +51 -0
- package/dist/tools/sheets/writeSpreadsheet.js +43 -0
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +93 -0
- package/dist/tools/utils/index.js +6 -0
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +154 -0
- package/dist/types.js +186 -0
- package/package.json +30 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
export function register(server) {
|
|
5
|
+
server.addTool({
|
|
6
|
+
name: 'renameSheet',
|
|
7
|
+
description: 'Renames a sheet (tab) in a spreadsheet. Use getSpreadsheetInfo to find the numeric sheet ID.',
|
|
8
|
+
parameters: z.object({
|
|
9
|
+
spreadsheetId: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
12
|
+
sheetId: z
|
|
13
|
+
.number()
|
|
14
|
+
.int()
|
|
15
|
+
.describe('The numeric sheet ID to rename. Use getSpreadsheetInfo to find sheet IDs.'),
|
|
16
|
+
newName: z.string().min(1).describe('The new name for the sheet.'),
|
|
17
|
+
}),
|
|
18
|
+
execute: async (args, { log }) => {
|
|
19
|
+
const sheets = await getSheetsClient();
|
|
20
|
+
log.info(`Renaming sheet ID ${args.sheetId} to "${args.newName}" in spreadsheet ${args.spreadsheetId}`);
|
|
21
|
+
try {
|
|
22
|
+
await sheets.spreadsheets.batchUpdate({
|
|
23
|
+
spreadsheetId: args.spreadsheetId,
|
|
24
|
+
requestBody: {
|
|
25
|
+
requests: [
|
|
26
|
+
{
|
|
27
|
+
updateSheetProperties: {
|
|
28
|
+
properties: {
|
|
29
|
+
sheetId: args.sheetId,
|
|
30
|
+
title: args.newName,
|
|
31
|
+
},
|
|
32
|
+
fields: 'title',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
return `Successfully renamed sheet (ID: ${args.sheetId}) to "${args.newName}".`;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
log.error(`Error renaming sheet in spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
42
|
+
if (error instanceof UserError)
|
|
43
|
+
throw error;
|
|
44
|
+
throw new UserError(`Failed to rename sheet: ${error.message || 'Unknown error'}`);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'setColumnWidths',
|
|
8
|
+
description: 'Sets the width (in pixels) of one or more columns in a spreadsheet. Accepts multiple column specs in a single call, each targeting a single column or a contiguous range (e.g., "A", "B:D").',
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
spreadsheetId: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
13
|
+
sheetName: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Name of the sheet/tab. Defaults to the first sheet if not provided.'),
|
|
17
|
+
columnWidths: z
|
|
18
|
+
.array(z.object({
|
|
19
|
+
column: z
|
|
20
|
+
.string()
|
|
21
|
+
.describe('Column or column range in A1 notation (e.g., "A", "B:D").'),
|
|
22
|
+
width: z.number().int().min(0).describe('Width in pixels. Use 0 to hide the column.'),
|
|
23
|
+
}))
|
|
24
|
+
.min(1)
|
|
25
|
+
.describe('List of column width specifications to apply.'),
|
|
26
|
+
}),
|
|
27
|
+
execute: async (args, { log }) => {
|
|
28
|
+
const sheets = await getSheetsClient();
|
|
29
|
+
log.info(`Setting column widths in spreadsheet ${args.spreadsheetId}`);
|
|
30
|
+
try {
|
|
31
|
+
await SheetsHelpers.setColumnWidths(sheets, args.spreadsheetId, args.sheetName, args.columnWidths);
|
|
32
|
+
const summary = args.columnWidths.map((cw) => `${cw.column}=${cw.width}px`).join(', ');
|
|
33
|
+
return `Successfully set column widths: ${summary}.`;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
log.error(`Error setting column widths: ${error.message || error}`);
|
|
37
|
+
if (error instanceof UserError)
|
|
38
|
+
throw error;
|
|
39
|
+
throw new UserError(`Failed to set column widths: ${error.message || 'Unknown error'}`);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'setDropdownValidation',
|
|
8
|
+
description: 'Adds or removes a dropdown list on a range of cells. Provide values to create a dropdown restricting input to those options. Omit values to remove dropdown validation from the range.',
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
spreadsheetId: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
13
|
+
range: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe('A1 notation range to apply the dropdown to (e.g., "Sheet1!B2:B100" or "C2:C50").'),
|
|
16
|
+
values: z
|
|
17
|
+
.array(z.string())
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('The allowed dropdown options (e.g., ["Open", "In Progress", "Done"]). Omit to remove existing dropdown validation from the range.'),
|
|
20
|
+
strict: z
|
|
21
|
+
.boolean()
|
|
22
|
+
.optional()
|
|
23
|
+
.default(true)
|
|
24
|
+
.describe('If true, reject input that does not match one of the dropdown values.'),
|
|
25
|
+
inputMessage: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Help text shown when a cell with the dropdown is selected.'),
|
|
29
|
+
}),
|
|
30
|
+
execute: async (args, { log }) => {
|
|
31
|
+
const sheets = await getSheetsClient();
|
|
32
|
+
const isClearing = !args.values || args.values.length === 0;
|
|
33
|
+
log.info(isClearing
|
|
34
|
+
? `Clearing dropdown validation on "${args.range}" in spreadsheet ${args.spreadsheetId}`
|
|
35
|
+
: `Setting dropdown validation on "${args.range}" with ${args.values.length} options in spreadsheet ${args.spreadsheetId}`);
|
|
36
|
+
try {
|
|
37
|
+
await SheetsHelpers.setDropdownValidation(sheets, args.spreadsheetId, args.range, args.values, args.strict, args.inputMessage);
|
|
38
|
+
if (isClearing) {
|
|
39
|
+
return `Successfully removed dropdown validation from range "${args.range}".`;
|
|
40
|
+
}
|
|
41
|
+
return `Successfully added dropdown validation to range "${args.range}" with ${args.values.length} options: ${args.values.join(', ')}.`;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
log.error(`Error setting dropdown validation: ${error.message || error}`);
|
|
45
|
+
if (error instanceof UserError)
|
|
46
|
+
throw error;
|
|
47
|
+
throw new UserError(`Failed to set dropdown validation: ${error.message || 'Unknown error'}`);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'ungroupAllRows',
|
|
8
|
+
description: 'Removes all row groupings from a sheet by deleting the entire row dimension group. Use before re-running groupRows to prevent duplicate collapse widgets from accumulating across report refreshes.',
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
spreadsheetId: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
13
|
+
sheetName: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Name of the sheet/tab. Defaults to the first sheet if not provided.'),
|
|
17
|
+
totalRows: z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.optional()
|
|
21
|
+
.default(500)
|
|
22
|
+
.describe('Number of rows to clear groups from (default: 500). Set higher if the sheet has more rows.'),
|
|
23
|
+
}),
|
|
24
|
+
execute: async (args, { log }) => {
|
|
25
|
+
const sheets = await getSheetsClient();
|
|
26
|
+
log.info(`Removing all row groups from spreadsheet ${args.spreadsheetId}`);
|
|
27
|
+
try {
|
|
28
|
+
const sheetId = await SheetsHelpers.resolveSheetId(sheets, args.spreadsheetId, args.sheetName);
|
|
29
|
+
const totalRows = args.totalRows ?? 500;
|
|
30
|
+
const request = {
|
|
31
|
+
deleteDimensionGroup: {
|
|
32
|
+
range: {
|
|
33
|
+
sheetId,
|
|
34
|
+
dimension: 'ROWS',
|
|
35
|
+
startIndex: 0,
|
|
36
|
+
endIndex: totalRows,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
// deleteDimensionGroup removes one level at a time; call repeatedly until no groups remain
|
|
41
|
+
let removed = 0;
|
|
42
|
+
// eslint-disable-next-line no-constant-condition
|
|
43
|
+
while (true) {
|
|
44
|
+
try {
|
|
45
|
+
await sheets.spreadsheets.batchUpdate({
|
|
46
|
+
spreadsheetId: args.spreadsheetId,
|
|
47
|
+
requestBody: { requests: [request] },
|
|
48
|
+
});
|
|
49
|
+
removed++;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
// When no groups remain the API returns an error — treat as done
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return `Successfully removed all row groups (${removed} level(s) cleared).`;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
log.error(`Error removing row groups: ${error.message || error}`);
|
|
60
|
+
if (error instanceof UserError)
|
|
61
|
+
throw error;
|
|
62
|
+
throw new UserError(`Failed to remove row groups: ${error.message || 'Unknown error'}`);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'updateTableRange',
|
|
8
|
+
description: "Modifies a table's dimensions (add/remove rows and columns) by updating its range. The new range must include the original table range.",
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
spreadsheetId: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
13
|
+
tableIdentifier: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe('The table name or table ID to update. Use listTables to see available tables.'),
|
|
16
|
+
range: z
|
|
17
|
+
.string()
|
|
18
|
+
.describe('New A1 notation range for the table (e.g., "Sheet1!A1:F15"). Must include the original table range.'),
|
|
19
|
+
}),
|
|
20
|
+
execute: async (args, { log }) => {
|
|
21
|
+
const sheets = await getSheetsClient();
|
|
22
|
+
log.info(`Updating table range for "${args.tableIdentifier}": ${args.range}`);
|
|
23
|
+
try {
|
|
24
|
+
// Resolve the table to get its current info
|
|
25
|
+
const { table, sheetName } = await SheetsHelpers.resolveTableIdentifier(sheets, args.spreadsheetId, args.tableIdentifier);
|
|
26
|
+
// Parse the new range
|
|
27
|
+
const { a1Range } = SheetsHelpers.parseRange(args.range);
|
|
28
|
+
const sheetId = await SheetsHelpers.resolveSheetId(sheets, args.spreadsheetId, sheetName || undefined);
|
|
29
|
+
const newRange = SheetsHelpers.parseA1ToGridRange(a1Range, sheetId);
|
|
30
|
+
// Update the table range
|
|
31
|
+
const updatedTable = await SheetsHelpers.updateTableRangeHelper(sheets, args.spreadsheetId, table.tableId || '', newRange);
|
|
32
|
+
return JSON.stringify({
|
|
33
|
+
tableId: updatedTable.tableId,
|
|
34
|
+
name: updatedTable.name,
|
|
35
|
+
oldRange: table.range
|
|
36
|
+
? `${SheetsHelpers.rowColToA1(table.range.startRowIndex || 0, table.range.startColumnIndex || 0)}:${SheetsHelpers.rowColToA1((table.range.endRowIndex || 1) - 1, (table.range.endColumnIndex || 1) - 1)}`
|
|
37
|
+
: 'Unknown',
|
|
38
|
+
newRange: args.range,
|
|
39
|
+
columnCount: updatedTable.columnProperties?.length || 0,
|
|
40
|
+
message: `Table "${updatedTable.name}" range updated successfully.`,
|
|
41
|
+
}, null, 2);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
log.error(`Error updating table range: ${error.message || error}`);
|
|
45
|
+
if (error instanceof UserError)
|
|
46
|
+
throw error;
|
|
47
|
+
throw new UserError(`Failed to update table range: ${error.message || 'Unknown error'}`);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'writeSpreadsheet',
|
|
8
|
+
description: 'Writes data to a range in a spreadsheet, overwriting existing values. Use appendRows to add data without overwriting.',
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
spreadsheetId: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
13
|
+
range: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe('A1 notation range to write to (e.g., "A1:B2" or "Sheet1!A1:B2").'),
|
|
16
|
+
values: z
|
|
17
|
+
.array(z.array(z.any()))
|
|
18
|
+
.describe('2D array of values to write. Each inner array represents a row.'),
|
|
19
|
+
valueInputOption: z
|
|
20
|
+
.enum(['RAW', 'USER_ENTERED'])
|
|
21
|
+
.optional()
|
|
22
|
+
.default('USER_ENTERED')
|
|
23
|
+
.describe('How input data should be interpreted. RAW: values are stored as-is. USER_ENTERED: values are parsed as if typed by a user.'),
|
|
24
|
+
}),
|
|
25
|
+
execute: async (args, { log }) => {
|
|
26
|
+
const sheets = await getSheetsClient();
|
|
27
|
+
log.info(`Writing to spreadsheet ${args.spreadsheetId}, range: ${args.range}`);
|
|
28
|
+
try {
|
|
29
|
+
const response = await SheetsHelpers.writeRange(sheets, args.spreadsheetId, args.range, args.values, args.valueInputOption);
|
|
30
|
+
const updatedCells = response.updatedCells || 0;
|
|
31
|
+
const updatedRows = response.updatedRows || 0;
|
|
32
|
+
const updatedColumns = response.updatedColumns || 0;
|
|
33
|
+
return `Successfully wrote ${updatedCells} cells (${updatedRows} rows, ${updatedColumns} columns) to range ${args.range}.`;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
log.error(`Error writing to spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
37
|
+
if (error instanceof UserError)
|
|
38
|
+
throw error;
|
|
39
|
+
throw new UserError(`Failed to write to spreadsheet: ${error.message || 'Unknown error'}`);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDocsClient } from '../../clients.js';
|
|
4
|
+
import { DocumentIdParameter, MarkdownConversionError } from '../../types.js';
|
|
5
|
+
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
+
import { insertMarkdown, formatInsertResult } from '../../markdown-transformer/index.js';
|
|
7
|
+
export function register(server) {
|
|
8
|
+
server.addTool({
|
|
9
|
+
name: 'appendMarkdown',
|
|
10
|
+
description: 'Appends formatted content to the end of a document using markdown syntax. Supports headings, bold, italic, strikethrough, links, and bullet/numbered lists. Use this instead of appendText when you need formatting.',
|
|
11
|
+
parameters: DocumentIdParameter.extend({
|
|
12
|
+
markdown: z.string().min(1).describe('The markdown content to append.'),
|
|
13
|
+
addNewlineIfNeeded: z
|
|
14
|
+
.boolean()
|
|
15
|
+
.optional()
|
|
16
|
+
.default(true)
|
|
17
|
+
.describe('Add spacing before appended content if needed.'),
|
|
18
|
+
tabId: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('The ID of the specific tab to append to. If not specified, appends to the first tab.'),
|
|
22
|
+
firstHeadingAsTitle: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.optional()
|
|
25
|
+
.default(false)
|
|
26
|
+
.describe('If true, the first H1 heading (# ...) in the markdown is styled as a Google Docs TITLE instead of Heading 1. Useful when the markdown represents a full document whose first line is the document title.'),
|
|
27
|
+
}),
|
|
28
|
+
execute: async (args, { log }) => {
|
|
29
|
+
const docs = await getDocsClient();
|
|
30
|
+
log.info(`Appending markdown to doc ${args.documentId} (${args.markdown.length} chars)${args.tabId ? ` in tab ${args.tabId}` : ''}`);
|
|
31
|
+
try {
|
|
32
|
+
// 1. Get document end index
|
|
33
|
+
const doc = await docs.documents.get({
|
|
34
|
+
documentId: args.documentId,
|
|
35
|
+
includeTabsContent: !!args.tabId,
|
|
36
|
+
fields: args.tabId ? 'tabs' : 'body(content(endIndex))',
|
|
37
|
+
});
|
|
38
|
+
let bodyContent;
|
|
39
|
+
if (args.tabId) {
|
|
40
|
+
const targetTab = GDocsHelpers.findTabById(doc.data, args.tabId);
|
|
41
|
+
if (!targetTab) {
|
|
42
|
+
throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
|
|
43
|
+
}
|
|
44
|
+
if (!targetTab.documentTab) {
|
|
45
|
+
throw new UserError(`Tab "${args.tabId}" does not have content (may not be a document tab).`);
|
|
46
|
+
}
|
|
47
|
+
bodyContent = targetTab.documentTab.body?.content;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
bodyContent = doc.data.body?.content;
|
|
51
|
+
}
|
|
52
|
+
if (!bodyContent) {
|
|
53
|
+
throw new UserError('No content found in document/tab');
|
|
54
|
+
}
|
|
55
|
+
let startIndex = bodyContent[bodyContent.length - 1].endIndex - 1;
|
|
56
|
+
log.info(`Document end index: ${startIndex}`);
|
|
57
|
+
// 2. Add spacing if needed
|
|
58
|
+
if (args.addNewlineIfNeeded && startIndex > 1) {
|
|
59
|
+
const location = { index: startIndex };
|
|
60
|
+
if (args.tabId) {
|
|
61
|
+
location.tabId = args.tabId;
|
|
62
|
+
}
|
|
63
|
+
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [
|
|
64
|
+
{
|
|
65
|
+
insertText: {
|
|
66
|
+
location,
|
|
67
|
+
text: '\n\n',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
startIndex += 2;
|
|
72
|
+
log.info(`Added spacing, new start index: ${startIndex}`);
|
|
73
|
+
}
|
|
74
|
+
// 3. Convert and append markdown
|
|
75
|
+
const result = await insertMarkdown(docs, args.documentId, args.markdown, {
|
|
76
|
+
startIndex,
|
|
77
|
+
tabId: args.tabId,
|
|
78
|
+
firstHeadingAsTitle: args.firstHeadingAsTitle,
|
|
79
|
+
});
|
|
80
|
+
const debugSummary = formatInsertResult(result);
|
|
81
|
+
log.info(debugSummary);
|
|
82
|
+
return `Successfully appended ${args.markdown.length} characters of markdown.\n\n${debugSummary}`;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
log.error(`Error appending markdown: ${error.message}`);
|
|
86
|
+
if (error instanceof UserError || error instanceof MarkdownConversionError) {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
throw new UserError(`Failed to append markdown: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { register as replaceDocumentWithMarkdown } from './replaceDocumentWithMarkdown.js';
|
|
2
|
+
import { register as appendMarkdownToGoogleDoc } from './appendMarkdownToGoogleDoc.js';
|
|
3
|
+
export function registerUtilsTools(server) {
|
|
4
|
+
replaceDocumentWithMarkdown(server);
|
|
5
|
+
appendMarkdownToGoogleDoc(server);
|
|
6
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDocsClient } from '../../clients.js';
|
|
4
|
+
import { DocumentIdParameter, MarkdownConversionError } from '../../types.js';
|
|
5
|
+
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
+
import { insertMarkdown, formatInsertResult } from '../../markdown-transformer/index.js';
|
|
7
|
+
export function register(server) {
|
|
8
|
+
server.addTool({
|
|
9
|
+
name: 'replaceDocumentWithMarkdown',
|
|
10
|
+
description: "Replaces the entire document body with content parsed from markdown. Supports headings, bold, italic, strikethrough, links, and bullet/numbered lists. Use readDocument with format='markdown' first to get the current content, edit it, then call this tool to apply changes.",
|
|
11
|
+
parameters: DocumentIdParameter.extend({
|
|
12
|
+
markdown: z.string().min(1).describe('The markdown content to apply to the document.'),
|
|
13
|
+
preserveTitle: z
|
|
14
|
+
.boolean()
|
|
15
|
+
.optional()
|
|
16
|
+
.default(false)
|
|
17
|
+
.describe('If true, preserves the first heading/title and replaces content after it.'),
|
|
18
|
+
tabId: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('The ID of the specific tab to replace content in. If not specified, replaces content in the first tab.'),
|
|
22
|
+
firstHeadingAsTitle: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.optional()
|
|
25
|
+
.default(true)
|
|
26
|
+
.describe('If true (default), the first H1 heading (# ...) in the markdown is styled as a Google Docs TITLE instead of Heading 1. Useful when the markdown represents a full document whose first line is the document title. Set to false if the first H1 should remain a Heading 1.'),
|
|
27
|
+
}),
|
|
28
|
+
execute: async (args, { log }) => {
|
|
29
|
+
const docs = await getDocsClient();
|
|
30
|
+
log.info(`Replacing doc ${args.documentId} with markdown (${args.markdown.length} chars)${args.tabId ? ` in tab ${args.tabId}` : ''}`);
|
|
31
|
+
try {
|
|
32
|
+
// 1. Get document structure
|
|
33
|
+
const doc = await docs.documents.get({
|
|
34
|
+
documentId: args.documentId,
|
|
35
|
+
includeTabsContent: !!args.tabId,
|
|
36
|
+
fields: args.tabId ? 'tabs' : 'body(content(startIndex,endIndex))',
|
|
37
|
+
});
|
|
38
|
+
// 2. Calculate replacement range
|
|
39
|
+
let startIndex = 1;
|
|
40
|
+
let bodyContent;
|
|
41
|
+
if (args.tabId) {
|
|
42
|
+
const targetTab = GDocsHelpers.findTabById(doc.data, args.tabId);
|
|
43
|
+
if (!targetTab) {
|
|
44
|
+
throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
|
|
45
|
+
}
|
|
46
|
+
if (!targetTab.documentTab) {
|
|
47
|
+
throw new UserError(`Tab "${args.tabId}" does not have content (may not be a document tab).`);
|
|
48
|
+
}
|
|
49
|
+
bodyContent = targetTab.documentTab.body?.content;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
bodyContent = doc.data.body?.content;
|
|
53
|
+
}
|
|
54
|
+
if (!bodyContent) {
|
|
55
|
+
throw new UserError('No content found in document/tab');
|
|
56
|
+
}
|
|
57
|
+
let endIndex = bodyContent[bodyContent.length - 1].endIndex - 1;
|
|
58
|
+
if (args.preserveTitle) {
|
|
59
|
+
// Find first content element that's a heading or paragraph
|
|
60
|
+
for (const element of bodyContent) {
|
|
61
|
+
if (element.paragraph && element.endIndex) {
|
|
62
|
+
startIndex = element.endIndex;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 3. Delete existing content
|
|
68
|
+
if (endIndex > startIndex) {
|
|
69
|
+
const deleteRange = { startIndex, endIndex };
|
|
70
|
+
if (args.tabId) {
|
|
71
|
+
deleteRange.tabId = args.tabId;
|
|
72
|
+
}
|
|
73
|
+
log.info(`Deleting content from index ${startIndex} to ${endIndex}`);
|
|
74
|
+
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [
|
|
75
|
+
{
|
|
76
|
+
deleteContentRange: { range: deleteRange },
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
log.info(`Delete complete.`);
|
|
80
|
+
}
|
|
81
|
+
// 4. Clean the surviving trailing paragraph.
|
|
82
|
+
// deleteContentRange always leaves one trailing paragraph that cannot
|
|
83
|
+
// be deleted. If it has bullet list membership or text formatting from
|
|
84
|
+
// the old content, all subsequently inserted text inherits those
|
|
85
|
+
// properties, corrupting the new document. We strip both bullets and
|
|
86
|
+
// text styles from the survivor before inserting.
|
|
87
|
+
{
|
|
88
|
+
// Re-read to get the survivor's endIndex (always a short document now)
|
|
89
|
+
const docAfterDelete = await docs.documents.get({
|
|
90
|
+
documentId: args.documentId,
|
|
91
|
+
includeTabsContent: !!args.tabId,
|
|
92
|
+
fields: args.tabId ? 'tabs' : 'body(content(startIndex,endIndex))',
|
|
93
|
+
});
|
|
94
|
+
let survivorContent;
|
|
95
|
+
if (args.tabId) {
|
|
96
|
+
const tab = GDocsHelpers.findTabById(docAfterDelete.data, args.tabId);
|
|
97
|
+
survivorContent = tab?.documentTab?.body?.content;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
survivorContent = docAfterDelete.data.body?.content;
|
|
101
|
+
}
|
|
102
|
+
const survivorEnd = survivorContent
|
|
103
|
+
? survivorContent[survivorContent.length - 1].endIndex
|
|
104
|
+
: startIndex + 1;
|
|
105
|
+
const survivorRange = { startIndex, endIndex: survivorEnd };
|
|
106
|
+
if (args.tabId) {
|
|
107
|
+
survivorRange.tabId = args.tabId;
|
|
108
|
+
}
|
|
109
|
+
const cleanupRequests = [
|
|
110
|
+
{ deleteParagraphBullets: { range: survivorRange } },
|
|
111
|
+
{
|
|
112
|
+
updateTextStyle: {
|
|
113
|
+
range: survivorRange,
|
|
114
|
+
textStyle: {
|
|
115
|
+
underline: false,
|
|
116
|
+
bold: false,
|
|
117
|
+
italic: false,
|
|
118
|
+
strikethrough: false,
|
|
119
|
+
foregroundColor: {},
|
|
120
|
+
backgroundColor: {},
|
|
121
|
+
},
|
|
122
|
+
fields: 'underline,bold,italic,strikethrough,foregroundColor,backgroundColor',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
try {
|
|
127
|
+
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, cleanupRequests);
|
|
128
|
+
log.info(`Cleaned surviving paragraph (bullets + text style) at range ${startIndex}-${survivorEnd}`);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
log.info(`Survivor cleanup skipped: ${e.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// 5. Convert markdown and insert (indices calculated for empty document)
|
|
135
|
+
log.info(`Inserting markdown starting at index ${startIndex} (after delete, document should be empty)`);
|
|
136
|
+
const result = await insertMarkdown(docs, args.documentId, args.markdown, {
|
|
137
|
+
startIndex,
|
|
138
|
+
tabId: args.tabId,
|
|
139
|
+
firstHeadingAsTitle: args.firstHeadingAsTitle,
|
|
140
|
+
});
|
|
141
|
+
const debugSummary = formatInsertResult(result);
|
|
142
|
+
log.info(debugSummary);
|
|
143
|
+
return `Successfully replaced document content with ${args.markdown.length} characters of markdown.\n\n${debugSummary}`;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
log.error(`Error replacing document with markdown: ${error.message}`);
|
|
147
|
+
if (error instanceof UserError || error instanceof MarkdownConversionError) {
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
throw new UserError(`Failed to apply markdown: ${error.message || 'Unknown error'}`);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|