@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,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.strictObject({
|
|
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,45 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
import { SpreadsheetCellValueSchema } from '../../types.js';
|
|
6
|
+
export function register(server) {
|
|
7
|
+
server.addTool({
|
|
8
|
+
name: 'writeSpreadsheet',
|
|
9
|
+
description: 'Writes data to a range in a spreadsheet, overwriting existing values. Use appendRows to add data without overwriting.',
|
|
10
|
+
parameters: z.strictObject({
|
|
11
|
+
spreadsheetId: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
14
|
+
range: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe('A1 notation range to write to (e.g., "A1:B2" or "Sheet1!A1:B2").'),
|
|
17
|
+
values: z
|
|
18
|
+
.array(z.array(SpreadsheetCellValueSchema).max(500))
|
|
19
|
+
.max(500)
|
|
20
|
+
.describe('2D array of values to write. Each inner array represents a row.'),
|
|
21
|
+
valueInputOption: z
|
|
22
|
+
.enum(['RAW', 'USER_ENTERED'])
|
|
23
|
+
.optional()
|
|
24
|
+
.default('USER_ENTERED')
|
|
25
|
+
.describe('How input data should be interpreted. RAW: values are stored as-is. USER_ENTERED: values are parsed as if typed by a user.'),
|
|
26
|
+
}),
|
|
27
|
+
execute: async (args, { log }) => {
|
|
28
|
+
const sheets = await getSheetsClient();
|
|
29
|
+
log.info(`Writing to spreadsheet ${args.spreadsheetId}, range: ${args.range}`);
|
|
30
|
+
try {
|
|
31
|
+
const response = await SheetsHelpers.writeRange(sheets, args.spreadsheetId, args.range, args.values, args.valueInputOption);
|
|
32
|
+
const updatedCells = response.updatedCells || 0;
|
|
33
|
+
const updatedRows = response.updatedRows || 0;
|
|
34
|
+
const updatedColumns = response.updatedColumns || 0;
|
|
35
|
+
return `Successfully wrote ${updatedCells} cells (${updatedRows} rows, ${updatedColumns} columns) to range ${args.range}.`;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
log.error(`Error writing to spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
39
|
+
if (error instanceof UserError)
|
|
40
|
+
throw error;
|
|
41
|
+
throw new UserError(`Failed to write to spreadsheet: ${error.message || 'Unknown error'}`);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
import { TAB_BODY_END_INDEX_FIELDS } from '../docs/tabFieldMasks.js';
|
|
8
|
+
export function register(server) {
|
|
9
|
+
server.addTool({
|
|
10
|
+
name: 'appendMarkdown',
|
|
11
|
+
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.',
|
|
12
|
+
parameters: DocumentIdParameter.extend({
|
|
13
|
+
markdown: z.string().min(1).describe('The markdown content to append.'),
|
|
14
|
+
addNewlineIfNeeded: z
|
|
15
|
+
.boolean()
|
|
16
|
+
.optional()
|
|
17
|
+
.default(true)
|
|
18
|
+
.describe('Add spacing before appended content if needed.'),
|
|
19
|
+
tabId: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('The ID of the specific tab to append to. If not specified, appends to the first tab.'),
|
|
23
|
+
firstHeadingAsTitle: z
|
|
24
|
+
.boolean()
|
|
25
|
+
.optional()
|
|
26
|
+
.default(false)
|
|
27
|
+
.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.'),
|
|
28
|
+
}),
|
|
29
|
+
execute: async (args, { log }) => {
|
|
30
|
+
const docs = await getDocsClient();
|
|
31
|
+
log.info(`Appending markdown to doc ${args.documentId} (${args.markdown.length} chars)${args.tabId ? ` in tab ${args.tabId}` : ''}`);
|
|
32
|
+
try {
|
|
33
|
+
// 1. Get document end index
|
|
34
|
+
const doc = await docs.documents.get({
|
|
35
|
+
documentId: args.documentId,
|
|
36
|
+
includeTabsContent: !!args.tabId,
|
|
37
|
+
suggestionsViewMode: 'PREVIEW_WITHOUT_SUGGESTIONS',
|
|
38
|
+
fields: args.tabId ? TAB_BODY_END_INDEX_FIELDS : 'body(content(endIndex))',
|
|
39
|
+
});
|
|
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 startIndex = bodyContent[bodyContent.length - 1].endIndex - 1;
|
|
58
|
+
log.info(`Document end index: ${startIndex}`);
|
|
59
|
+
// 2. Add spacing if needed
|
|
60
|
+
if (args.addNewlineIfNeeded && startIndex > 1) {
|
|
61
|
+
const location = { index: startIndex };
|
|
62
|
+
if (args.tabId) {
|
|
63
|
+
location.tabId = args.tabId;
|
|
64
|
+
}
|
|
65
|
+
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [
|
|
66
|
+
{
|
|
67
|
+
insertText: {
|
|
68
|
+
location,
|
|
69
|
+
text: '\n\n',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
startIndex += 2;
|
|
74
|
+
log.info(`Added spacing, new start index: ${startIndex}`);
|
|
75
|
+
}
|
|
76
|
+
// 3. Convert and append markdown
|
|
77
|
+
const result = await insertMarkdown(docs, args.documentId, args.markdown, {
|
|
78
|
+
startIndex,
|
|
79
|
+
tabId: args.tabId,
|
|
80
|
+
firstHeadingAsTitle: args.firstHeadingAsTitle,
|
|
81
|
+
});
|
|
82
|
+
const debugSummary = formatInsertResult(result);
|
|
83
|
+
log.info(debugSummary);
|
|
84
|
+
return `Successfully appended ${args.markdown.length} characters of markdown.\n\n${debugSummary}`;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
log.error(`Error appending markdown: ${error.message}`);
|
|
88
|
+
if (error instanceof UserError || error instanceof MarkdownConversionError) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
throw new UserError(`Failed to append markdown: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { register as replaceDocumentWithMarkdown } from './replaceDocumentWithMarkdown.js';
|
|
2
|
+
import { register as appendMarkdownToGoogleDoc } from './appendMarkdownToGoogleDoc.js';
|
|
3
|
+
import { register as replaceRangeWithMarkdown } from './replaceRangeWithMarkdown.js';
|
|
4
|
+
export function registerUtilsTools(server) {
|
|
5
|
+
replaceDocumentWithMarkdown(server);
|
|
6
|
+
appendMarkdownToGoogleDoc(server);
|
|
7
|
+
replaceRangeWithMarkdown(server);
|
|
8
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
import { TAB_BODY_RANGE_FIELDS } from '../docs/tabFieldMasks.js';
|
|
8
|
+
export function register(server) {
|
|
9
|
+
server.addTool({
|
|
10
|
+
name: 'replaceDocumentWithMarkdown',
|
|
11
|
+
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.",
|
|
12
|
+
parameters: DocumentIdParameter.extend({
|
|
13
|
+
markdown: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(1)
|
|
16
|
+
.max(500000)
|
|
17
|
+
.describe('The markdown content to apply to the document.'),
|
|
18
|
+
preserveTitle: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.optional()
|
|
21
|
+
.default(false)
|
|
22
|
+
.describe('If true, preserves the first heading/title and replaces content after it.'),
|
|
23
|
+
tabId: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('The ID of the specific tab to replace content in. If not specified, replaces content in the first tab.'),
|
|
27
|
+
firstHeadingAsTitle: z
|
|
28
|
+
.boolean()
|
|
29
|
+
.optional()
|
|
30
|
+
.default(true)
|
|
31
|
+
.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.'),
|
|
32
|
+
}),
|
|
33
|
+
execute: async (args, { log }) => {
|
|
34
|
+
const docs = await getDocsClient();
|
|
35
|
+
log.info(`Replacing doc ${args.documentId} with markdown (${args.markdown.length} chars)${args.tabId ? ` in tab ${args.tabId}` : ''}`);
|
|
36
|
+
try {
|
|
37
|
+
// 1. Get document structure
|
|
38
|
+
const doc = await docs.documents.get({
|
|
39
|
+
documentId: args.documentId,
|
|
40
|
+
includeTabsContent: !!args.tabId,
|
|
41
|
+
suggestionsViewMode: 'PREVIEW_WITHOUT_SUGGESTIONS',
|
|
42
|
+
fields: args.tabId ? TAB_BODY_RANGE_FIELDS : 'body(content(startIndex,endIndex))',
|
|
43
|
+
});
|
|
44
|
+
// 2. Calculate replacement range
|
|
45
|
+
let startIndex = 1;
|
|
46
|
+
let bodyContent;
|
|
47
|
+
if (args.tabId) {
|
|
48
|
+
const targetTab = GDocsHelpers.findTabById(doc.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
|
+
bodyContent = targetTab.documentTab.body?.content;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
bodyContent = doc.data.body?.content;
|
|
59
|
+
}
|
|
60
|
+
if (!bodyContent) {
|
|
61
|
+
throw new UserError('No content found in document/tab');
|
|
62
|
+
}
|
|
63
|
+
let endIndex = bodyContent[bodyContent.length - 1].endIndex - 1;
|
|
64
|
+
if (args.preserveTitle) {
|
|
65
|
+
// Find first content element that's a heading or paragraph
|
|
66
|
+
for (const element of bodyContent) {
|
|
67
|
+
if (element.paragraph && element.endIndex) {
|
|
68
|
+
startIndex = element.endIndex;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// 3. Delete existing content
|
|
74
|
+
if (endIndex > startIndex) {
|
|
75
|
+
const deleteRange = { startIndex, endIndex };
|
|
76
|
+
if (args.tabId) {
|
|
77
|
+
deleteRange.tabId = args.tabId;
|
|
78
|
+
}
|
|
79
|
+
log.info(`Deleting content from index ${startIndex} to ${endIndex}`);
|
|
80
|
+
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [
|
|
81
|
+
{
|
|
82
|
+
deleteContentRange: { range: deleteRange },
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
log.info(`Delete complete.`);
|
|
86
|
+
}
|
|
87
|
+
// 4. Clean the surviving trailing paragraph.
|
|
88
|
+
// deleteContentRange always leaves one trailing paragraph that cannot
|
|
89
|
+
// be deleted. If it has bullet list membership or text formatting from
|
|
90
|
+
// the old content, all subsequently inserted text inherits those
|
|
91
|
+
// properties, corrupting the new document. We strip both bullets and
|
|
92
|
+
// text styles from the survivor before inserting.
|
|
93
|
+
{
|
|
94
|
+
// Re-read to get the survivor's endIndex (always a short document now)
|
|
95
|
+
const docAfterDelete = await docs.documents.get({
|
|
96
|
+
documentId: args.documentId,
|
|
97
|
+
includeTabsContent: !!args.tabId,
|
|
98
|
+
suggestionsViewMode: 'PREVIEW_WITHOUT_SUGGESTIONS',
|
|
99
|
+
fields: args.tabId ? TAB_BODY_RANGE_FIELDS : 'body(content(startIndex,endIndex))',
|
|
100
|
+
});
|
|
101
|
+
let survivorContent;
|
|
102
|
+
if (args.tabId) {
|
|
103
|
+
const tab = GDocsHelpers.findTabById(docAfterDelete.data, args.tabId);
|
|
104
|
+
survivorContent = tab?.documentTab?.body?.content;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
survivorContent = docAfterDelete.data.body?.content;
|
|
108
|
+
}
|
|
109
|
+
const survivorEnd = survivorContent
|
|
110
|
+
? survivorContent[survivorContent.length - 1].endIndex
|
|
111
|
+
: startIndex + 1;
|
|
112
|
+
const survivorRange = { startIndex, endIndex: survivorEnd };
|
|
113
|
+
if (args.tabId) {
|
|
114
|
+
survivorRange.tabId = args.tabId;
|
|
115
|
+
}
|
|
116
|
+
const cleanupRequests = [
|
|
117
|
+
{ deleteParagraphBullets: { range: survivorRange } },
|
|
118
|
+
{
|
|
119
|
+
updateTextStyle: {
|
|
120
|
+
range: survivorRange,
|
|
121
|
+
textStyle: {
|
|
122
|
+
underline: false,
|
|
123
|
+
bold: false,
|
|
124
|
+
italic: false,
|
|
125
|
+
strikethrough: false,
|
|
126
|
+
foregroundColor: {},
|
|
127
|
+
backgroundColor: {},
|
|
128
|
+
},
|
|
129
|
+
fields: 'underline,bold,italic,strikethrough,foregroundColor,backgroundColor',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
try {
|
|
134
|
+
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, cleanupRequests);
|
|
135
|
+
log.info(`Cleaned surviving paragraph (bullets + text style) at range ${startIndex}-${survivorEnd}`);
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
log.info(`Survivor cleanup skipped: ${e.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// 5. Convert markdown and insert (indices calculated for empty document)
|
|
142
|
+
log.info(`Inserting markdown starting at index ${startIndex} (after delete, document should be empty)`);
|
|
143
|
+
const result = await insertMarkdown(docs, args.documentId, args.markdown, {
|
|
144
|
+
startIndex,
|
|
145
|
+
tabId: args.tabId,
|
|
146
|
+
firstHeadingAsTitle: args.firstHeadingAsTitle,
|
|
147
|
+
});
|
|
148
|
+
const debugSummary = formatInsertResult(result);
|
|
149
|
+
log.info(debugSummary);
|
|
150
|
+
return `Successfully replaced document content with ${args.markdown.length} characters of markdown.\n\n${debugSummary}`;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
log.error(`Error replacing document with markdown: ${error.message}`);
|
|
154
|
+
if (error instanceof UserError || error instanceof MarkdownConversionError) {
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
throw new UserError(`Failed to apply markdown: ${error.message || 'Unknown error'}`);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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: 'replaceRangeWithMarkdown',
|
|
10
|
+
description: "Replaces a specific character range in a document with formatted markdown content. Use readDocument with format='json' to determine the start and end indices of the content you want to replace. Supports headings, bold, italic, strikethrough, links, bullet/numbered lists, code blocks, horizontal rules, and tables.",
|
|
11
|
+
parameters: DocumentIdParameter.extend({
|
|
12
|
+
startIndex: z
|
|
13
|
+
.number()
|
|
14
|
+
.int()
|
|
15
|
+
.min(1)
|
|
16
|
+
.describe("1-based character index where the replacement range begins (inclusive). Use readDocument with format='json' to find the correct index."),
|
|
17
|
+
endIndex: z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.min(1)
|
|
21
|
+
.describe("1-based character index where the replacement range ends (exclusive). Use readDocument with format='json' to find the correct index."),
|
|
22
|
+
markdown: z
|
|
23
|
+
.string()
|
|
24
|
+
.min(1)
|
|
25
|
+
.describe('The markdown content to insert in place of the deleted range.'),
|
|
26
|
+
tabId: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('The ID of the specific tab to modify. If not specified, modifies the first tab.'),
|
|
30
|
+
}).refine((data) => data.endIndex > data.startIndex, {
|
|
31
|
+
message: 'endIndex must be greater than startIndex',
|
|
32
|
+
path: ['endIndex'],
|
|
33
|
+
}),
|
|
34
|
+
execute: async (args, { log }) => {
|
|
35
|
+
const docs = await getDocsClient();
|
|
36
|
+
log.info(`Replacing range ${args.startIndex}-${args.endIndex} in doc ${args.documentId} with markdown (${args.markdown.length} chars)${args.tabId ? ` in tab ${args.tabId}` : ''}`);
|
|
37
|
+
try {
|
|
38
|
+
// 1. Delete the existing content in the specified range
|
|
39
|
+
const deleteRange = {
|
|
40
|
+
startIndex: args.startIndex,
|
|
41
|
+
endIndex: args.endIndex,
|
|
42
|
+
};
|
|
43
|
+
if (args.tabId) {
|
|
44
|
+
deleteRange.tabId = args.tabId;
|
|
45
|
+
}
|
|
46
|
+
log.info(`Deleting content from index ${args.startIndex} to ${args.endIndex}`);
|
|
47
|
+
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [
|
|
48
|
+
{
|
|
49
|
+
deleteContentRange: { range: deleteRange },
|
|
50
|
+
},
|
|
51
|
+
]);
|
|
52
|
+
log.info(`Delete complete.`);
|
|
53
|
+
// 2. Insert markdown at the start position (which is now where the deleted content was)
|
|
54
|
+
log.info(`Inserting markdown at index ${args.startIndex}`);
|
|
55
|
+
const result = await insertMarkdown(docs, args.documentId, args.markdown, {
|
|
56
|
+
startIndex: args.startIndex,
|
|
57
|
+
tabId: args.tabId,
|
|
58
|
+
});
|
|
59
|
+
const debugSummary = formatInsertResult(result);
|
|
60
|
+
log.info(debugSummary);
|
|
61
|
+
return `Successfully replaced range ${args.startIndex}-${args.endIndex} with ${args.markdown.length} characters of markdown.\n\n${debugSummary}`;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
log.error(`Error replacing range with markdown: ${error.message}`);
|
|
65
|
+
if (error instanceof UserError || error instanceof MarkdownConversionError) {
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
throw new UserError(`Failed to replace range with markdown: ${error.message || 'Unknown error'}`);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
// --- Helper function for hex color validation ---
|
|
4
|
+
export const hexColorRegex = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
|
|
5
|
+
export const validateHexColor = (color) => hexColorRegex.test(color);
|
|
6
|
+
// --- Helper function for Hex to RGB conversion ---
|
|
7
|
+
export function hexToRgbColor(hex) {
|
|
8
|
+
if (!hex)
|
|
9
|
+
return null;
|
|
10
|
+
let hexClean = hex.startsWith('#') ? hex.slice(1) : hex;
|
|
11
|
+
if (hexClean.length === 3) {
|
|
12
|
+
hexClean = hexClean[0] + hexClean[0] + hexClean[1] + hexClean[1] + hexClean[2] + hexClean[2];
|
|
13
|
+
}
|
|
14
|
+
if (hexClean.length !== 6)
|
|
15
|
+
return null;
|
|
16
|
+
const bigint = parseInt(hexClean, 16);
|
|
17
|
+
if (isNaN(bigint))
|
|
18
|
+
return null;
|
|
19
|
+
const r = ((bigint >> 16) & 255) / 255;
|
|
20
|
+
const g = ((bigint >> 8) & 255) / 255;
|
|
21
|
+
const b = (bigint & 255) / 255;
|
|
22
|
+
return { red: r, green: g, blue: b };
|
|
23
|
+
}
|
|
24
|
+
// --- Zod Schema Fragments for Reusability ---
|
|
25
|
+
export const DocumentIdParameter = z.strictObject({
|
|
26
|
+
documentId: z
|
|
27
|
+
.string()
|
|
28
|
+
.describe('The document ID — the long string between /d/ and /edit in a Google Docs URL.'),
|
|
29
|
+
});
|
|
30
|
+
export const RangeParameters = z
|
|
31
|
+
.object({
|
|
32
|
+
startIndex: z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.min(1)
|
|
36
|
+
.describe('The starting index of the text range (inclusive, starts from 1).'),
|
|
37
|
+
endIndex: z.number().int().min(1).describe('The ending index of the text range (exclusive).'),
|
|
38
|
+
})
|
|
39
|
+
.refine((data) => data.endIndex > data.startIndex, {
|
|
40
|
+
message: 'endIndex must be greater than startIndex',
|
|
41
|
+
path: ['endIndex'],
|
|
42
|
+
});
|
|
43
|
+
export const OptionalRangeParameters = z
|
|
44
|
+
.object({
|
|
45
|
+
startIndex: z
|
|
46
|
+
.number()
|
|
47
|
+
.int()
|
|
48
|
+
.min(1)
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('Optional: The starting index of the text range (inclusive, starts from 1). If omitted, might apply to a found element or whole paragraph.'),
|
|
51
|
+
endIndex: z
|
|
52
|
+
.number()
|
|
53
|
+
.int()
|
|
54
|
+
.min(1)
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('Optional: The ending index of the text range (exclusive). If omitted, might apply to a found element or whole paragraph.'),
|
|
57
|
+
})
|
|
58
|
+
.refine((data) => !data.startIndex || !data.endIndex || data.endIndex > data.startIndex, {
|
|
59
|
+
message: 'If both startIndex and endIndex are provided, endIndex must be greater than startIndex',
|
|
60
|
+
path: ['endIndex'],
|
|
61
|
+
});
|
|
62
|
+
export const TextFindParameter = z.strictObject({
|
|
63
|
+
textToFind: z.string().min(1).describe('The exact text string to locate.'),
|
|
64
|
+
matchInstance: z
|
|
65
|
+
.number()
|
|
66
|
+
.int()
|
|
67
|
+
.min(1)
|
|
68
|
+
.optional()
|
|
69
|
+
.default(1)
|
|
70
|
+
.describe('Which instance of the text to target (1st, 2nd, etc.). Defaults to 1.'),
|
|
71
|
+
});
|
|
72
|
+
// --- Style Parameter Schemas ---
|
|
73
|
+
export const TextStyleParameters = z
|
|
74
|
+
.object({
|
|
75
|
+
bold: z.boolean().optional().describe('Apply bold formatting.'),
|
|
76
|
+
italic: z.boolean().optional().describe('Apply italic formatting.'),
|
|
77
|
+
underline: z.boolean().optional().describe('Apply underline formatting.'),
|
|
78
|
+
strikethrough: z.boolean().optional().describe('Apply strikethrough formatting.'),
|
|
79
|
+
fontSize: z.number().min(1).optional().describe('Set font size (in points, e.g., 12).'),
|
|
80
|
+
fontFamily: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('Set font family (e.g., "Arial", "Times New Roman").'),
|
|
84
|
+
foregroundColor: z
|
|
85
|
+
.string()
|
|
86
|
+
.refine(validateHexColor, { message: 'Invalid hex color format (e.g., #FF0000 or #F00)' })
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('Set text color using hex format (e.g., "#FF0000").'),
|
|
89
|
+
backgroundColor: z
|
|
90
|
+
.string()
|
|
91
|
+
.refine(validateHexColor, { message: 'Invalid hex color format (e.g., #00FF00 or #0F0)' })
|
|
92
|
+
.optional()
|
|
93
|
+
.describe('Set text background color using hex format (e.g., "#FFFF00").'),
|
|
94
|
+
linkUrl: z
|
|
95
|
+
.string()
|
|
96
|
+
.url()
|
|
97
|
+
.refine((url) => /^https?:\/\//i.test(url), { message: 'Only http/https URLs are allowed.' })
|
|
98
|
+
.optional()
|
|
99
|
+
.describe('Make the text a hyperlink pointing to this URL (http or https only).'),
|
|
100
|
+
// clearDirectFormatting: z.boolean().optional().describe('If true, attempts to clear all direct text formatting within the range before applying new styles.') // Harder to implement perfectly
|
|
101
|
+
})
|
|
102
|
+
.describe('Parameters for character-level text formatting.');
|
|
103
|
+
export const ParagraphStyleParameters = z
|
|
104
|
+
.object({
|
|
105
|
+
alignment: z
|
|
106
|
+
.enum(['START', 'END', 'CENTER', 'JUSTIFIED'])
|
|
107
|
+
.optional()
|
|
108
|
+
.describe('Paragraph alignment. START=left for LTR languages, END=right for LTR languages.'),
|
|
109
|
+
indentStart: z.number().min(0).optional().describe('Left indentation in points.'),
|
|
110
|
+
indentEnd: z.number().min(0).optional().describe('Right indentation in points.'),
|
|
111
|
+
spaceAbove: z.number().min(0).optional().describe('Space before the paragraph in points.'),
|
|
112
|
+
spaceBelow: z.number().min(0).optional().describe('Space after the paragraph in points.'),
|
|
113
|
+
namedStyleType: z
|
|
114
|
+
.enum([
|
|
115
|
+
'NORMAL_TEXT',
|
|
116
|
+
'TITLE',
|
|
117
|
+
'SUBTITLE',
|
|
118
|
+
'HEADING_1',
|
|
119
|
+
'HEADING_2',
|
|
120
|
+
'HEADING_3',
|
|
121
|
+
'HEADING_4',
|
|
122
|
+
'HEADING_5',
|
|
123
|
+
'HEADING_6',
|
|
124
|
+
])
|
|
125
|
+
.optional()
|
|
126
|
+
.describe('Apply a built-in named paragraph style (e.g., HEADING_1).'),
|
|
127
|
+
keepWithNext: z
|
|
128
|
+
.boolean()
|
|
129
|
+
.optional()
|
|
130
|
+
.describe('Keep this paragraph together with the next one on the same page.'),
|
|
131
|
+
// Borders are more complex, might need separate objects/tools
|
|
132
|
+
// clearDirectFormatting: z.boolean().optional().describe('If true, attempts to clear all direct paragraph formatting within the range before applying new styles.') // Harder to implement perfectly
|
|
133
|
+
})
|
|
134
|
+
.describe('Parameters for paragraph-level formatting.');
|
|
135
|
+
export const SpreadsheetCellValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
136
|
+
// --- Combination Schemas for Tools ---
|
|
137
|
+
export const ApplyTextStyleToolParameters = DocumentIdParameter.extend({
|
|
138
|
+
// Target EITHER by range OR by finding text
|
|
139
|
+
target: z
|
|
140
|
+
.union([RangeParameters, TextFindParameter])
|
|
141
|
+
.describe('Specify the target range either by start/end indices or by finding specific text.'),
|
|
142
|
+
style: TextStyleParameters.refine((styleArgs) => Object.values(styleArgs).some((v) => v !== undefined), { message: 'At least one text style option must be provided.' }).describe('The text styling to apply.'),
|
|
143
|
+
tabId: z
|
|
144
|
+
.string()
|
|
145
|
+
.optional()
|
|
146
|
+
.describe('The ID of the specific tab to apply formatting in. Use listDocumentTabs to get tab IDs. If not specified, operates on the first tab.'),
|
|
147
|
+
});
|
|
148
|
+
export const BatchApplyTextStyleOperation = z.strictObject({
|
|
149
|
+
target: z
|
|
150
|
+
.union([RangeParameters, TextFindParameter])
|
|
151
|
+
.describe("This operation's target range, either by start/end indices or by finding specific text."),
|
|
152
|
+
style: TextStyleParameters.refine((styleArgs) => Object.values(styleArgs).some((v) => v !== undefined), { message: 'At least one text style option must be provided.' }).describe('The text styling to apply for this operation.'),
|
|
153
|
+
});
|
|
154
|
+
export const BatchApplyTextStyleToolParameters = DocumentIdParameter.extend({
|
|
155
|
+
operations: z
|
|
156
|
+
.array(BatchApplyTextStyleOperation)
|
|
157
|
+
.min(1)
|
|
158
|
+
.max(500)
|
|
159
|
+
.describe('List of {target, style} operations to apply in as few Docs API calls as possible. ' +
|
|
160
|
+
'Prefer startIndex/endIndex ranges (computed once from a single readDocument JSON pass) over textToFind ' +
|
|
161
|
+
'— each textToFind target is resolved with its own document read before the batch write, which adds ' +
|
|
162
|
+
'back the round-trips this tool exists to avoid.'),
|
|
163
|
+
tabId: z
|
|
164
|
+
.string()
|
|
165
|
+
.optional()
|
|
166
|
+
.describe('The ID of the specific tab to apply formatting in. Applies to every operation in this call. If not specified, operates on the first tab.'),
|
|
167
|
+
});
|
|
168
|
+
export const ApplyParagraphStyleToolParameters = DocumentIdParameter.extend({
|
|
169
|
+
// Target EITHER by range OR by finding text (tool logic needs to find paragraph boundaries)
|
|
170
|
+
target: z
|
|
171
|
+
.union([
|
|
172
|
+
RangeParameters, // User provides paragraph start/end (less likely)
|
|
173
|
+
TextFindParameter, // Find text within paragraph to apply style
|
|
174
|
+
z.strictObject({
|
|
175
|
+
// Target by specific index within the paragraph
|
|
176
|
+
indexWithinParagraph: z
|
|
177
|
+
.number()
|
|
178
|
+
.int()
|
|
179
|
+
.min(1)
|
|
180
|
+
.describe('An index located anywhere within the target paragraph.'),
|
|
181
|
+
}),
|
|
182
|
+
])
|
|
183
|
+
.describe('Specify the target paragraph either by start/end indices, by finding text within it, or by providing an index within it.'),
|
|
184
|
+
style: ParagraphStyleParameters.refine((styleArgs) => Object.values(styleArgs).some((v) => v !== undefined), { message: 'At least one paragraph style option must be provided.' }).describe('The paragraph styling to apply.'),
|
|
185
|
+
tabId: z
|
|
186
|
+
.string()
|
|
187
|
+
.optional()
|
|
188
|
+
.describe('The ID of the specific tab to apply formatting in. Use listDocumentTabs to get tab IDs. If not specified, operates on the first tab.'),
|
|
189
|
+
});
|
|
190
|
+
// --- Error Classes ---
|
|
191
|
+
// Use FastMCP's UserError for client-facing issues
|
|
192
|
+
// Define custom errors for internal issues if needed
|
|
193
|
+
export class NotImplementedError extends Error {
|
|
194
|
+
constructor(message = 'This feature is not yet implemented.') {
|
|
195
|
+
super(message);
|
|
196
|
+
this.name = 'NotImplementedError';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export class MarkdownConversionError extends Error {
|
|
200
|
+
markdownPosition;
|
|
201
|
+
tokenType;
|
|
202
|
+
constructor(message, markdownPosition, tokenType) {
|
|
203
|
+
super(message);
|
|
204
|
+
this.markdownPosition = markdownPosition;
|
|
205
|
+
this.tokenType = tokenType;
|
|
206
|
+
this.name = 'MarkdownConversionError';
|
|
207
|
+
}
|
|
208
|
+
}
|