google-tools-mcp 1.2.11 → 2.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/LICENSE +21 -21
- package/README.md +555 -276
- package/dist/cachedToolsList.js +52 -52
- package/dist/googleDocsApiHelpers.js +76 -12
- package/dist/helpers.js +572 -252
- package/dist/httpAuth.js +296 -0
- package/dist/index.js +162 -11
- package/dist/logger.js +87 -87
- package/dist/markdown-transformer/docsToMarkdown.js +222 -21
- package/dist/markdown-transformer/index.js +19 -9
- package/dist/markdown-transformer/markdownToDocs.js +458 -28
- package/dist/readTracker.js +136 -17
- package/dist/sessionContext.js +37 -0
- package/dist/setup.js +187 -9
- package/dist/tools/calendar/getBusy.js +64 -64
- package/dist/tools/calendar/getEvents.js +141 -141
- package/dist/tools/calendar/getFree.js +225 -225
- package/dist/tools/calendar/index.js +19 -19
- package/dist/tools/calendar/listCalendars.js +38 -38
- package/dist/tools/calendar/listRecurringInstances.js +83 -83
- package/dist/tools/calendar/manageCalendar.js +121 -121
- package/dist/tools/calendar/manageEvent.js +258 -258
- package/dist/tools/calendar/moveEvent.js +66 -66
- package/dist/tools/docs/addTab.js +10 -12
- package/dist/tools/docs/appendToGoogleDoc.js +9 -4
- package/dist/tools/docs/deleteRange.js +9 -4
- package/dist/tools/docs/findAndReplace.js +9 -4
- package/dist/tools/docs/formatting/applyParagraphStyle.js +4 -1
- package/dist/tools/docs/formatting/getFormatting.js +171 -171
- package/dist/tools/docs/insertImage.js +15 -2
- package/dist/tools/docs/insertPageBreak.js +4 -1
- package/dist/tools/docs/insertTable.js +4 -1
- package/dist/tools/docs/insertTableWithData.js +4 -1
- package/dist/tools/docs/modifyText.js +14 -4
- package/dist/tools/docs/modifyText.test.js +84 -84
- package/dist/tools/docs/readGoogleDoc.js +71 -11
- package/dist/tools/docs/renameTab.js +12 -14
- package/dist/tools/drafts.js +165 -165
- package/dist/tools/drive/createDocument.js +13 -1
- package/dist/tools/drive/downloadFile.js +268 -268
- package/dist/tools/drive/getFileInfo.js +48 -48
- package/dist/tools/drive/getFilePath.js +50 -50
- package/dist/tools/drive/listDriveFiles.js +112 -112
- package/dist/tools/drive/listSharedDrives.js +52 -52
- package/dist/tools/drive/listSharedWithMe.js +97 -97
- package/dist/tools/drive/uploadFile.js +111 -111
- package/dist/tools/extras/index.js +9 -9
- package/dist/tools/extras/readDriveFile.js +212 -206
- package/dist/tools/extras/readFile.js +84 -84
- package/dist/tools/extras/searchFileContents.js +81 -81
- package/dist/tools/forms/batchUpdateForm.js +81 -81
- package/dist/tools/forms/createForm.js +54 -54
- package/dist/tools/forms/getForm.js +118 -118
- package/dist/tools/forms/getFormResponse.js +45 -45
- package/dist/tools/forms/index.js +15 -15
- package/dist/tools/forms/listFormResponses.js +56 -56
- package/dist/tools/forms/setPublishSettings.js +59 -59
- package/dist/tools/gmail/drafts.js +165 -165
- package/dist/tools/gmail/labels.js +54 -83
- package/dist/tools/gmail/messages.js +444 -441
- package/dist/tools/gmail/settings.js +358 -528
- package/dist/tools/gmail/threads.js +286 -164
- package/dist/tools/index.js +511 -473
- package/dist/tools/labels.js +103 -103
- package/dist/tools/legacyAliases.js +426 -0
- package/dist/tools/maps/directions.js +72 -0
- package/dist/tools/maps/geocode.js +16 -0
- package/dist/tools/maps/index.js +15 -0
- package/dist/tools/maps/mapsClient.js +83 -0
- package/dist/tools/maps/placeDetails.js +16 -0
- package/dist/tools/maps/reverseGeocode.js +19 -0
- package/dist/tools/maps/searchNearby.js +54 -0
- package/dist/tools/maps/searchPlaces.js +24 -0
- package/dist/tools/messages.js +448 -448
- package/dist/tools/settings.js +528 -528
- package/dist/tools/slides/createPresentation.js +104 -104
- package/dist/tools/slides/createShape.js +92 -92
- package/dist/tools/slides/createTextBox.js +82 -82
- package/dist/tools/slides/deleteSlide.js +30 -30
- package/dist/tools/slides/duplicateSlide.js +37 -37
- package/dist/tools/slides/exportThumbnail.js +42 -42
- package/dist/tools/slides/formatParagraph.js +72 -72
- package/dist/tools/slides/formatText.js +84 -84
- package/dist/tools/slides/getPresentation.js +87 -87
- package/dist/tools/slides/index.js +33 -33
- package/dist/tools/slides/reorderSlides.js +41 -41
- package/dist/tools/slides/replaceAllText.js +46 -46
- package/dist/tools/slides/setBackground.js +58 -58
- package/dist/tools/slides/speakerNotes.js +102 -102
- package/dist/tools/slides/styleShape.js +111 -111
- package/dist/tools/slides/updatePresentation.js +128 -128
- package/dist/tools/threads.js +145 -145
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +31 -7
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +72 -9
- package/dist/updateCheck.js +202 -0
- package/dist/workspace.js +122 -0
- package/package.json +82 -80
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { getDocsClient } from '../../clients.js';
|
|
4
4
|
import { DocumentIdParameter } from '../../types.js';
|
|
5
5
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
+
import { getLastReadRevisionId, trackMutation } from '../../readTracker.js';
|
|
6
7
|
export function register(server) {
|
|
7
8
|
server.addTool({
|
|
8
9
|
name: 'addTab',
|
|
@@ -52,19 +53,16 @@ export function register(server) {
|
|
|
52
53
|
tabProperties.index = args.index;
|
|
53
54
|
if (args.iconEmoji !== undefined)
|
|
54
55
|
tabProperties.iconEmoji = args.iconEmoji;
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
tabProperties,
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
],
|
|
56
|
+
const revisionId = getLastReadRevisionId(args.documentId);
|
|
57
|
+
const response = await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [
|
|
58
|
+
{
|
|
59
|
+
addDocumentTab: {
|
|
60
|
+
tabProperties,
|
|
61
|
+
},
|
|
65
62
|
},
|
|
66
|
-
});
|
|
67
|
-
|
|
63
|
+
], revisionId ? { requiredRevisionId: revisionId } : undefined);
|
|
64
|
+
trackMutation(args.documentId, response?.writeControl?.requiredRevisionId);
|
|
65
|
+
const newTabProps = response.replies?.[0]?.addDocumentTab?.tabProperties;
|
|
68
66
|
if (newTabProps) {
|
|
69
67
|
return JSON.stringify({
|
|
70
68
|
url: `https://docs.google.com/document/d/${args.documentId}/edit`,
|
|
@@ -5,7 +5,7 @@ import { getDocsClient } from '../../clients.js';
|
|
|
5
5
|
import { DocumentIdParameter, NotImplementedError } from '../../types.js';
|
|
6
6
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
7
7
|
import { docsJsonToMarkdown } from '../../markdown-transformer/index.js';
|
|
8
|
-
import { guardMutation, trackMutation } from '../../readTracker.js';
|
|
8
|
+
import { guardMutation, getLastReadRevisionId, trackMutation } from '../../readTracker.js';
|
|
9
9
|
export function register(server) {
|
|
10
10
|
server.addTool({
|
|
11
11
|
name: 'appendText',
|
|
@@ -29,7 +29,11 @@ export function register(server) {
|
|
|
29
29
|
await guardMutation(args.documentId, {
|
|
30
30
|
contentFetcher: async () => {
|
|
31
31
|
const current = await docs.documents.get({ documentId: args.documentId });
|
|
32
|
-
|
|
32
|
+
// Return the revision this content came from alongside the
|
|
33
|
+
// content itself so guardMutation can refresh both together
|
|
34
|
+
// instead of leaving revisionId stale after a diff (see
|
|
35
|
+
// readTracker.js guardMutation for why that matters).
|
|
36
|
+
return { content: docsJsonToMarkdown(current.data), revisionId: current.data.revisionId };
|
|
33
37
|
},
|
|
34
38
|
});
|
|
35
39
|
// Resolve text content from filePath or inline parameter
|
|
@@ -88,8 +92,9 @@ export function register(server) {
|
|
|
88
92
|
const request = {
|
|
89
93
|
insertText: { location, text: textToInsert },
|
|
90
94
|
};
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
const revisionId = getLastReadRevisionId(args.documentId);
|
|
96
|
+
const writeResponse = await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [request], revisionId ? { requiredRevisionId: revisionId } : undefined);
|
|
97
|
+
trackMutation(args.documentId, writeResponse?.writeControl?.requiredRevisionId);
|
|
93
98
|
log.info(`Successfully appended to doc: ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
|
|
94
99
|
const docUrl = `https://docs.google.com/document/d/${args.documentId}/edit`;
|
|
95
100
|
return `${docUrl}\nSuccessfully appended text to ${args.tabId ? `tab ${args.tabId} in ` : ''}document ${args.documentId}.`;
|
|
@@ -4,7 +4,7 @@ import { getDocsClient } from '../../clients.js';
|
|
|
4
4
|
import { DocumentIdParameter } from '../../types.js';
|
|
5
5
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
6
|
import { docsJsonToMarkdown } from '../../markdown-transformer/index.js';
|
|
7
|
-
import { guardMutation, trackMutation } from '../../readTracker.js';
|
|
7
|
+
import { guardMutation, getLastReadRevisionId, trackMutation } from '../../readTracker.js';
|
|
8
8
|
export function register(server) {
|
|
9
9
|
server.addTool({
|
|
10
10
|
name: 'deleteRange',
|
|
@@ -33,7 +33,11 @@ export function register(server) {
|
|
|
33
33
|
await guardMutation(args.documentId, {
|
|
34
34
|
contentFetcher: async () => {
|
|
35
35
|
const current = await docs.documents.get({ documentId: args.documentId });
|
|
36
|
-
|
|
36
|
+
// Return the revision this content came from alongside the
|
|
37
|
+
// content itself so guardMutation can refresh both together
|
|
38
|
+
// instead of leaving revisionId stale after a diff (see
|
|
39
|
+
// readTracker.js guardMutation for why that matters).
|
|
40
|
+
return { content: docsJsonToMarkdown(current.data), revisionId: current.data.revisionId };
|
|
37
41
|
},
|
|
38
42
|
});
|
|
39
43
|
log.info(`Deleting range ${args.startIndex}-${args.endIndex} in doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
|
|
@@ -66,8 +70,9 @@ export function register(server) {
|
|
|
66
70
|
const request = {
|
|
67
71
|
deleteContentRange: { range },
|
|
68
72
|
};
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
const revisionId = getLastReadRevisionId(args.documentId);
|
|
74
|
+
const writeResponse = await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [request], revisionId ? { requiredRevisionId: revisionId } : undefined);
|
|
75
|
+
trackMutation(args.documentId, writeResponse?.writeControl?.requiredRevisionId);
|
|
71
76
|
const docUrl = `https://docs.google.com/document/d/${args.documentId}/edit`;
|
|
72
77
|
return `${docUrl}\nSuccessfully deleted content in range ${args.startIndex}-${args.endIndex}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
|
|
73
78
|
}
|
|
@@ -4,7 +4,7 @@ import { getDocsClient } from '../../clients.js';
|
|
|
4
4
|
import { DocumentIdParameter } from '../../types.js';
|
|
5
5
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
6
|
import { docsJsonToMarkdown } from '../../markdown-transformer/index.js';
|
|
7
|
-
import { guardMutation, trackMutation } from '../../readTracker.js';
|
|
7
|
+
import { guardMutation, getLastReadRevisionId, trackMutation } from '../../readTracker.js';
|
|
8
8
|
const FindAndReplaceParameters = DocumentIdParameter.extend({
|
|
9
9
|
findText: z.string().min(1).describe('The text to search for in the document.'),
|
|
10
10
|
replaceText: z
|
|
@@ -30,7 +30,11 @@ export function register(server) {
|
|
|
30
30
|
await guardMutation(args.documentId, {
|
|
31
31
|
contentFetcher: async () => {
|
|
32
32
|
const current = await docs.documents.get({ documentId: args.documentId });
|
|
33
|
-
|
|
33
|
+
// Return the revision this content came from alongside the
|
|
34
|
+
// content itself so guardMutation can refresh both together
|
|
35
|
+
// instead of leaving revisionId stale after a diff (see
|
|
36
|
+
// readTracker.js guardMutation for why that matters).
|
|
37
|
+
return { content: docsJsonToMarkdown(current.data), revisionId: current.data.revisionId };
|
|
34
38
|
},
|
|
35
39
|
});
|
|
36
40
|
log.info(`findAndReplace in doc ${args.documentId}: "${args.findText}" → "${args.replaceText}"` +
|
|
@@ -47,8 +51,9 @@ export function register(server) {
|
|
|
47
51
|
...(args.tabId && { tabsCriteria: { tabIds: [args.tabId] } }),
|
|
48
52
|
},
|
|
49
53
|
};
|
|
50
|
-
const
|
|
51
|
-
|
|
54
|
+
const revisionId = getLastReadRevisionId(args.documentId);
|
|
55
|
+
const response = await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [request], revisionId ? { requiredRevisionId: revisionId } : undefined);
|
|
56
|
+
trackMutation(args.documentId, response?.writeControl?.requiredRevisionId);
|
|
52
57
|
const changed = response.replies?.[0]?.replaceAllText?.occurrencesChanged ?? 0;
|
|
53
58
|
const docUrl = `https://docs.google.com/document/d/${args.documentId}/edit`;
|
|
54
59
|
return `${docUrl}\nReplaced ${changed} occurrence(s) of "${args.findText}" with "${args.replaceText}".`;
|
|
@@ -2,6 +2,7 @@ import { UserError } from 'fastmcp';
|
|
|
2
2
|
import { getDocsClient } from '../../../clients.js';
|
|
3
3
|
import { ApplyParagraphStyleToolParameters, NotImplementedError, } from '../../../types.js';
|
|
4
4
|
import * as GDocsHelpers from '../../../googleDocsApiHelpers.js';
|
|
5
|
+
import { getLastReadRevisionId, trackMutation } from '../../../readTracker.js';
|
|
5
6
|
export function register(server) {
|
|
6
7
|
server.addTool({
|
|
7
8
|
name: 'applyParagraphStyle',
|
|
@@ -64,7 +65,9 @@ export function register(server) {
|
|
|
64
65
|
return 'No valid paragraph styling options were provided.';
|
|
65
66
|
}
|
|
66
67
|
log.info(`Applying styles: ${requestInfo.fields.join(', ')}`);
|
|
67
|
-
|
|
68
|
+
const revisionId = getLastReadRevisionId(args.documentId);
|
|
69
|
+
const writeResponse = await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [requestInfo.request], revisionId ? { requiredRevisionId: revisionId } : undefined);
|
|
70
|
+
trackMutation(args.documentId, writeResponse?.writeControl?.requiredRevisionId);
|
|
68
71
|
const docUrl = `https://docs.google.com/document/d/${args.documentId}/edit`;
|
|
69
72
|
return `${docUrl}\nSuccessfully applied paragraph styles (${requestInfo.fields.join(', ')}) to the paragraph${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
|
|
70
73
|
}
|
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
import { UserError } from 'fastmcp';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { getDocsClient } from '../../../clients.js';
|
|
4
|
-
import { DocumentIdParameter, TextFindParameter } from '../../../types.js';
|
|
5
|
-
import * as GDocsHelpers from '../../../googleDocsApiHelpers.js';
|
|
6
|
-
|
|
7
|
-
const RangeTarget = z.object({
|
|
8
|
-
startIndex: z.number().int().min(1).describe('Start of range (inclusive, 1-based).'),
|
|
9
|
-
endIndex: z.number().int().min(1).describe('End of range (exclusive).'),
|
|
10
|
-
}).refine((d) => d.endIndex > d.startIndex, {
|
|
11
|
-
message: 'endIndex must be greater than startIndex',
|
|
12
|
-
path: ['endIndex'],
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const GetFormattingParameters = DocumentIdParameter.extend({
|
|
16
|
-
target: z
|
|
17
|
-
.union([RangeTarget, TextFindParameter])
|
|
18
|
-
.describe('Target by range indices or text search.'),
|
|
19
|
-
tabId: z
|
|
20
|
-
.string()
|
|
21
|
-
.optional()
|
|
22
|
-
.describe('The ID of the specific tab to read from. If not specified, reads from the first tab.'),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Extracts text style and paragraph style info from document elements
|
|
27
|
-
* that overlap the given range.
|
|
28
|
-
*/
|
|
29
|
-
function extractFormattingFromContent(bodyContent, startIndex, endIndex) {
|
|
30
|
-
const textStyles = [];
|
|
31
|
-
const paragraphStyles = [];
|
|
32
|
-
|
|
33
|
-
for (const element of bodyContent) {
|
|
34
|
-
if (!element.paragraph) continue;
|
|
35
|
-
|
|
36
|
-
const para = element.paragraph;
|
|
37
|
-
// Check if this paragraph overlaps our range
|
|
38
|
-
const paraElements = para.elements || [];
|
|
39
|
-
let paragraphOverlaps = false;
|
|
40
|
-
|
|
41
|
-
for (const pe of paraElements) {
|
|
42
|
-
const elStart = pe.startIndex ?? 0;
|
|
43
|
-
const elEnd = pe.endIndex ?? 0;
|
|
44
|
-
|
|
45
|
-
// Check overlap with our target range
|
|
46
|
-
if (elEnd > startIndex && elStart < endIndex) {
|
|
47
|
-
paragraphOverlaps = true;
|
|
48
|
-
if (pe.textRun) {
|
|
49
|
-
const text = pe.textRun.content || '';
|
|
50
|
-
const style = pe.textRun.textStyle || {};
|
|
51
|
-
// Only include non-empty style properties
|
|
52
|
-
const cleanStyle = {};
|
|
53
|
-
if (style.bold) cleanStyle.bold = true;
|
|
54
|
-
if (style.italic) cleanStyle.italic = true;
|
|
55
|
-
if (style.underline) cleanStyle.underline = true;
|
|
56
|
-
if (style.strikethrough) cleanStyle.strikethrough = true;
|
|
57
|
-
if (style.fontSize) cleanStyle.fontSize = style.fontSize;
|
|
58
|
-
if (style.foregroundColor?.color?.rgbColor) cleanStyle.foregroundColor = style.foregroundColor.color.rgbColor;
|
|
59
|
-
if (style.backgroundColor?.color?.rgbColor) cleanStyle.backgroundColor = style.backgroundColor.color.rgbColor;
|
|
60
|
-
if (style.weightedFontFamily) cleanStyle.fontFamily = style.weightedFontFamily.fontFamily;
|
|
61
|
-
if (style.link) cleanStyle.link = style.link;
|
|
62
|
-
if (style.baselineOffset && style.baselineOffset !== 'BASELINE_OFFSET_UNSPECIFIED') cleanStyle.baselineOffset = style.baselineOffset;
|
|
63
|
-
|
|
64
|
-
textStyles.push({
|
|
65
|
-
startIndex: elStart,
|
|
66
|
-
endIndex: elEnd,
|
|
67
|
-
text: text.replace(/\n$/, ''),
|
|
68
|
-
style: cleanStyle,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (paragraphOverlaps && para.paragraphStyle) {
|
|
75
|
-
const ps = para.paragraphStyle;
|
|
76
|
-
const cleanStyle = {};
|
|
77
|
-
if (ps.namedStyleType && ps.namedStyleType !== 'NORMAL_TEXT') cleanStyle.namedStyleType = ps.namedStyleType;
|
|
78
|
-
if (ps.alignment && ps.alignment !== 'START') cleanStyle.alignment = ps.alignment;
|
|
79
|
-
if (ps.indentStart?.magnitude) cleanStyle.indentStart = ps.indentStart.magnitude;
|
|
80
|
-
if (ps.indentEnd?.magnitude) cleanStyle.indentEnd = ps.indentEnd.magnitude;
|
|
81
|
-
if (ps.spaceAbove?.magnitude) cleanStyle.spaceAbove = ps.spaceAbove.magnitude;
|
|
82
|
-
if (ps.spaceBelow?.magnitude) cleanStyle.spaceBelow = ps.spaceBelow.magnitude;
|
|
83
|
-
if (ps.keepWithNext) cleanStyle.keepWithNext = true;
|
|
84
|
-
if (ps.lineSpacing) cleanStyle.lineSpacing = ps.lineSpacing;
|
|
85
|
-
if (ps.direction && ps.direction !== 'LEFT_TO_RIGHT') cleanStyle.direction = ps.direction;
|
|
86
|
-
|
|
87
|
-
// Always include namedStyleType even if NORMAL_TEXT for clarity
|
|
88
|
-
if (!cleanStyle.namedStyleType) cleanStyle.namedStyleType = 'NORMAL_TEXT';
|
|
89
|
-
|
|
90
|
-
paragraphStyles.push({
|
|
91
|
-
paragraphStartIndex: element.startIndex,
|
|
92
|
-
paragraphEndIndex: element.endIndex,
|
|
93
|
-
style: cleanStyle,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return { textStyles, paragraphStyles };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function register(server) {
|
|
102
|
-
server.addTool({
|
|
103
|
-
name: 'getFormatting',
|
|
104
|
-
description: 'Returns the text styling (bold, italic, font, colors, etc.) and paragraph styling ' +
|
|
105
|
-
'(alignment, headings, spacing, indentation) for a specific range or found text in a Google Doc. ' +
|
|
106
|
-
'Returns per-run text styles and per-paragraph styles, so mixed formatting (e.g. bold + italic spans) is fully represented.',
|
|
107
|
-
parameters: GetFormattingParameters,
|
|
108
|
-
execute: async (args, { log }) => {
|
|
109
|
-
const docs = await getDocsClient();
|
|
110
|
-
log.info(`getFormatting on doc ${args.documentId}: target=${JSON.stringify(args.target)}` +
|
|
111
|
-
`${args.tabId ? `, tab=${args.tabId}` : ''}`);
|
|
112
|
-
try {
|
|
113
|
-
// Resolve target to numeric indices
|
|
114
|
-
let startIndex;
|
|
115
|
-
let endIndex;
|
|
116
|
-
|
|
117
|
-
if ('textToFind' in args.target) {
|
|
118
|
-
const range = await GDocsHelpers.findTextRange(
|
|
119
|
-
docs, args.documentId, args.target.textToFind,
|
|
120
|
-
args.target.matchInstance, args.tabId
|
|
121
|
-
);
|
|
122
|
-
if (!range) {
|
|
123
|
-
throw new UserError(
|
|
124
|
-
`Could not find instance ${args.target.matchInstance ?? 1} of text "${args.target.textToFind}"${args.tabId ? ` in tab ${args.tabId}` : ''}.`
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
startIndex = range.startIndex;
|
|
128
|
-
endIndex = range.endIndex;
|
|
129
|
-
} else {
|
|
130
|
-
startIndex = args.target.startIndex;
|
|
131
|
-
endIndex = args.target.endIndex;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Fetch document with full content
|
|
135
|
-
const needsTabsContent = !!args.tabId;
|
|
136
|
-
const res = await docs.documents.get({
|
|
137
|
-
documentId: args.documentId,
|
|
138
|
-
...(needsTabsContent && { includeTabsContent: true }),
|
|
139
|
-
fields: needsTabsContent ? '*' : 'body(content)',
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
let bodyContent;
|
|
143
|
-
if (args.tabId) {
|
|
144
|
-
const targetTab = GDocsHelpers.findTabById(res.data, args.tabId);
|
|
145
|
-
if (!targetTab) {
|
|
146
|
-
throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
|
|
147
|
-
}
|
|
148
|
-
bodyContent = targetTab.documentTab?.body?.content;
|
|
149
|
-
} else {
|
|
150
|
-
bodyContent = res.data.body?.content;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!bodyContent) {
|
|
154
|
-
throw new UserError('Document has no content.');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const result = extractFormattingFromContent(bodyContent, startIndex, endIndex);
|
|
158
|
-
|
|
159
|
-
return JSON.stringify({
|
|
160
|
-
range: { startIndex, endIndex },
|
|
161
|
-
textStyles: result.textStyles,
|
|
162
|
-
paragraphStyles: result.paragraphStyles,
|
|
163
|
-
}, null, 2);
|
|
164
|
-
} catch (error) {
|
|
165
|
-
log.error(`Error in getFormatting for doc ${args.documentId}: ${error.message || error}`);
|
|
166
|
-
if (error instanceof UserError) throw error;
|
|
167
|
-
throw new UserError(`Failed to get formatting: ${error.message || 'Unknown error'}`);
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
}
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDocsClient } from '../../../clients.js';
|
|
4
|
+
import { DocumentIdParameter, TextFindParameter } from '../../../types.js';
|
|
5
|
+
import * as GDocsHelpers from '../../../googleDocsApiHelpers.js';
|
|
6
|
+
|
|
7
|
+
const RangeTarget = z.object({
|
|
8
|
+
startIndex: z.number().int().min(1).describe('Start of range (inclusive, 1-based).'),
|
|
9
|
+
endIndex: z.number().int().min(1).describe('End of range (exclusive).'),
|
|
10
|
+
}).refine((d) => d.endIndex > d.startIndex, {
|
|
11
|
+
message: 'endIndex must be greater than startIndex',
|
|
12
|
+
path: ['endIndex'],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const GetFormattingParameters = DocumentIdParameter.extend({
|
|
16
|
+
target: z
|
|
17
|
+
.union([RangeTarget, TextFindParameter])
|
|
18
|
+
.describe('Target by range indices or text search.'),
|
|
19
|
+
tabId: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('The ID of the specific tab to read from. If not specified, reads from the first tab.'),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extracts text style and paragraph style info from document elements
|
|
27
|
+
* that overlap the given range.
|
|
28
|
+
*/
|
|
29
|
+
function extractFormattingFromContent(bodyContent, startIndex, endIndex) {
|
|
30
|
+
const textStyles = [];
|
|
31
|
+
const paragraphStyles = [];
|
|
32
|
+
|
|
33
|
+
for (const element of bodyContent) {
|
|
34
|
+
if (!element.paragraph) continue;
|
|
35
|
+
|
|
36
|
+
const para = element.paragraph;
|
|
37
|
+
// Check if this paragraph overlaps our range
|
|
38
|
+
const paraElements = para.elements || [];
|
|
39
|
+
let paragraphOverlaps = false;
|
|
40
|
+
|
|
41
|
+
for (const pe of paraElements) {
|
|
42
|
+
const elStart = pe.startIndex ?? 0;
|
|
43
|
+
const elEnd = pe.endIndex ?? 0;
|
|
44
|
+
|
|
45
|
+
// Check overlap with our target range
|
|
46
|
+
if (elEnd > startIndex && elStart < endIndex) {
|
|
47
|
+
paragraphOverlaps = true;
|
|
48
|
+
if (pe.textRun) {
|
|
49
|
+
const text = pe.textRun.content || '';
|
|
50
|
+
const style = pe.textRun.textStyle || {};
|
|
51
|
+
// Only include non-empty style properties
|
|
52
|
+
const cleanStyle = {};
|
|
53
|
+
if (style.bold) cleanStyle.bold = true;
|
|
54
|
+
if (style.italic) cleanStyle.italic = true;
|
|
55
|
+
if (style.underline) cleanStyle.underline = true;
|
|
56
|
+
if (style.strikethrough) cleanStyle.strikethrough = true;
|
|
57
|
+
if (style.fontSize) cleanStyle.fontSize = style.fontSize;
|
|
58
|
+
if (style.foregroundColor?.color?.rgbColor) cleanStyle.foregroundColor = style.foregroundColor.color.rgbColor;
|
|
59
|
+
if (style.backgroundColor?.color?.rgbColor) cleanStyle.backgroundColor = style.backgroundColor.color.rgbColor;
|
|
60
|
+
if (style.weightedFontFamily) cleanStyle.fontFamily = style.weightedFontFamily.fontFamily;
|
|
61
|
+
if (style.link) cleanStyle.link = style.link;
|
|
62
|
+
if (style.baselineOffset && style.baselineOffset !== 'BASELINE_OFFSET_UNSPECIFIED') cleanStyle.baselineOffset = style.baselineOffset;
|
|
63
|
+
|
|
64
|
+
textStyles.push({
|
|
65
|
+
startIndex: elStart,
|
|
66
|
+
endIndex: elEnd,
|
|
67
|
+
text: text.replace(/\n$/, ''),
|
|
68
|
+
style: cleanStyle,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (paragraphOverlaps && para.paragraphStyle) {
|
|
75
|
+
const ps = para.paragraphStyle;
|
|
76
|
+
const cleanStyle = {};
|
|
77
|
+
if (ps.namedStyleType && ps.namedStyleType !== 'NORMAL_TEXT') cleanStyle.namedStyleType = ps.namedStyleType;
|
|
78
|
+
if (ps.alignment && ps.alignment !== 'START') cleanStyle.alignment = ps.alignment;
|
|
79
|
+
if (ps.indentStart?.magnitude) cleanStyle.indentStart = ps.indentStart.magnitude;
|
|
80
|
+
if (ps.indentEnd?.magnitude) cleanStyle.indentEnd = ps.indentEnd.magnitude;
|
|
81
|
+
if (ps.spaceAbove?.magnitude) cleanStyle.spaceAbove = ps.spaceAbove.magnitude;
|
|
82
|
+
if (ps.spaceBelow?.magnitude) cleanStyle.spaceBelow = ps.spaceBelow.magnitude;
|
|
83
|
+
if (ps.keepWithNext) cleanStyle.keepWithNext = true;
|
|
84
|
+
if (ps.lineSpacing) cleanStyle.lineSpacing = ps.lineSpacing;
|
|
85
|
+
if (ps.direction && ps.direction !== 'LEFT_TO_RIGHT') cleanStyle.direction = ps.direction;
|
|
86
|
+
|
|
87
|
+
// Always include namedStyleType even if NORMAL_TEXT for clarity
|
|
88
|
+
if (!cleanStyle.namedStyleType) cleanStyle.namedStyleType = 'NORMAL_TEXT';
|
|
89
|
+
|
|
90
|
+
paragraphStyles.push({
|
|
91
|
+
paragraphStartIndex: element.startIndex,
|
|
92
|
+
paragraphEndIndex: element.endIndex,
|
|
93
|
+
style: cleanStyle,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { textStyles, paragraphStyles };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function register(server) {
|
|
102
|
+
server.addTool({
|
|
103
|
+
name: 'getFormatting',
|
|
104
|
+
description: 'Returns the text styling (bold, italic, font, colors, etc.) and paragraph styling ' +
|
|
105
|
+
'(alignment, headings, spacing, indentation) for a specific range or found text in a Google Doc. ' +
|
|
106
|
+
'Returns per-run text styles and per-paragraph styles, so mixed formatting (e.g. bold + italic spans) is fully represented.',
|
|
107
|
+
parameters: GetFormattingParameters,
|
|
108
|
+
execute: async (args, { log }) => {
|
|
109
|
+
const docs = await getDocsClient();
|
|
110
|
+
log.info(`getFormatting on doc ${args.documentId}: target=${JSON.stringify(args.target)}` +
|
|
111
|
+
`${args.tabId ? `, tab=${args.tabId}` : ''}`);
|
|
112
|
+
try {
|
|
113
|
+
// Resolve target to numeric indices
|
|
114
|
+
let startIndex;
|
|
115
|
+
let endIndex;
|
|
116
|
+
|
|
117
|
+
if ('textToFind' in args.target) {
|
|
118
|
+
const range = await GDocsHelpers.findTextRange(
|
|
119
|
+
docs, args.documentId, args.target.textToFind,
|
|
120
|
+
args.target.matchInstance, args.tabId
|
|
121
|
+
);
|
|
122
|
+
if (!range) {
|
|
123
|
+
throw new UserError(
|
|
124
|
+
`Could not find instance ${args.target.matchInstance ?? 1} of text "${args.target.textToFind}"${args.tabId ? ` in tab ${args.tabId}` : ''}.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
startIndex = range.startIndex;
|
|
128
|
+
endIndex = range.endIndex;
|
|
129
|
+
} else {
|
|
130
|
+
startIndex = args.target.startIndex;
|
|
131
|
+
endIndex = args.target.endIndex;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Fetch document with full content
|
|
135
|
+
const needsTabsContent = !!args.tabId;
|
|
136
|
+
const res = await docs.documents.get({
|
|
137
|
+
documentId: args.documentId,
|
|
138
|
+
...(needsTabsContent && { includeTabsContent: true }),
|
|
139
|
+
fields: needsTabsContent ? '*' : 'body(content)',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
let bodyContent;
|
|
143
|
+
if (args.tabId) {
|
|
144
|
+
const targetTab = GDocsHelpers.findTabById(res.data, args.tabId);
|
|
145
|
+
if (!targetTab) {
|
|
146
|
+
throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
|
|
147
|
+
}
|
|
148
|
+
bodyContent = targetTab.documentTab?.body?.content;
|
|
149
|
+
} else {
|
|
150
|
+
bodyContent = res.data.body?.content;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!bodyContent) {
|
|
154
|
+
throw new UserError('Document has no content.');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const result = extractFormattingFromContent(bodyContent, startIndex, endIndex);
|
|
158
|
+
|
|
159
|
+
return JSON.stringify({
|
|
160
|
+
range: { startIndex, endIndex },
|
|
161
|
+
textStyles: result.textStyles,
|
|
162
|
+
paragraphStyles: result.paragraphStyles,
|
|
163
|
+
}, null, 2);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
log.error(`Error in getFormatting for doc ${args.documentId}: ${error.message || error}`);
|
|
166
|
+
if (error instanceof UserError) throw error;
|
|
167
|
+
throw new UserError(`Failed to get formatting: ${error.message || 'Unknown error'}`);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { getDocsClient, getDriveClient, getScriptClient } from '../../clients.js';
|
|
4
4
|
import { DocumentIdParameter } from '../../types.js';
|
|
5
5
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
+
import { getLastReadRevisionId, requireRereadBeforeMutation, trackMutation } from '../../readTracker.js';
|
|
6
7
|
export function register(server) {
|
|
7
8
|
server.addTool({
|
|
8
9
|
name: 'insertImage',
|
|
@@ -75,7 +76,17 @@ export function register(server) {
|
|
|
75
76
|
const driveFileId = await GDocsHelpers.uploadImageToDrive(drive, args.localImagePath, parentFolderId, true // skipPublicSharing
|
|
76
77
|
);
|
|
77
78
|
log.info(`[AppsScript] Inserting image via marker at index ${args.index} (fileId: ${driveFileId})`);
|
|
78
|
-
|
|
79
|
+
const appsScriptRevisionId = getLastReadRevisionId(args.documentId);
|
|
80
|
+
await GDocsHelpers.insertImageViaAppsScript(docs, scriptClient, appsScriptDeploymentId, args.documentId, driveFileId, args.index, args.tabId, appsScriptRevisionId ? { requiredRevisionId: appsScriptRevisionId } : undefined);
|
|
81
|
+
// The Apps Script call that replaces the marker with the image mutates
|
|
82
|
+
// the document outside our batchUpdate visibility, so we have no way to
|
|
83
|
+
// learn the true post-write revision here. trackMutation would clear the
|
|
84
|
+
// revision and the modifiedTime, which leaves the next write with nothing
|
|
85
|
+
// to check against and sends it out unguarded on a pre-image baseline.
|
|
86
|
+
// Require a fresh read instead.
|
|
87
|
+
log.warn(`Document ${args.documentId} must be read again before the next write ` +
|
|
88
|
+
'(the Apps Script mutates the doc outside batchUpdate visibility, so no true post-write revision is available).');
|
|
89
|
+
requireRereadBeforeMutation(args.documentId, 'an Apps Script inserted an image and its resulting revision is not visible to us.');
|
|
79
90
|
const docUrl = `https://docs.google.com/document/d/${args.documentId}/edit`;
|
|
80
91
|
return `${docUrl}\nSuccessfully inserted local image at index ${args.index} via Apps Script${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
|
|
81
92
|
}
|
|
@@ -105,7 +116,9 @@ export function register(server) {
|
|
|
105
116
|
resolvedUrl = args.imageUrl;
|
|
106
117
|
log.info(`Inserting image from URL ${resolvedUrl} at index ${args.index} in doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
|
|
107
118
|
}
|
|
108
|
-
|
|
119
|
+
const revisionId = getLastReadRevisionId(args.documentId);
|
|
120
|
+
const writeResponse = await GDocsHelpers.insertInlineImage(docs, args.documentId, resolvedUrl, args.index, args.width, args.height, args.tabId, revisionId ? { requiredRevisionId: revisionId } : undefined);
|
|
121
|
+
trackMutation(args.documentId, writeResponse?.writeControl?.requiredRevisionId);
|
|
109
122
|
let sizeInfo = '';
|
|
110
123
|
if (args.width && args.height) {
|
|
111
124
|
sizeInfo = ` with size ${args.width}x${args.height}pt`;
|
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { getDocsClient } from '../../clients.js';
|
|
4
4
|
import { DocumentIdParameter } from '../../types.js';
|
|
5
5
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
+
import { getLastReadRevisionId, trackMutation } from '../../readTracker.js';
|
|
6
7
|
export function register(server) {
|
|
7
8
|
server.addTool({
|
|
8
9
|
name: 'insertPageBreak',
|
|
@@ -44,7 +45,9 @@ export function register(server) {
|
|
|
44
45
|
const request = {
|
|
45
46
|
insertPageBreak: { location },
|
|
46
47
|
};
|
|
47
|
-
|
|
48
|
+
const revisionId = getLastReadRevisionId(args.documentId);
|
|
49
|
+
const writeResponse = await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [request], revisionId ? { requiredRevisionId: revisionId } : undefined);
|
|
50
|
+
trackMutation(args.documentId, writeResponse?.writeControl?.requiredRevisionId);
|
|
48
51
|
const docUrl = `https://docs.google.com/document/d/${args.documentId}/edit`;
|
|
49
52
|
return `${docUrl}\nSuccessfully inserted page break at index ${args.index}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
|
|
50
53
|
}
|
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { getDocsClient } from '../../clients.js';
|
|
4
4
|
import { DocumentIdParameter } from '../../types.js';
|
|
5
5
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
+
import { getLastReadRevisionId, trackMutation } from '../../readTracker.js';
|
|
6
7
|
export function register(server) {
|
|
7
8
|
server.addTool({
|
|
8
9
|
name: 'insertTable',
|
|
@@ -39,7 +40,9 @@ export function register(server) {
|
|
|
39
40
|
throw new UserError(`Tab "${args.tabId}" does not have content (may not be a document tab).`);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
+
const revisionId = getLastReadRevisionId(args.documentId);
|
|
44
|
+
const writeResponse = await GDocsHelpers.createTable(docs, args.documentId, args.rows, args.columns, args.index, args.tabId, revisionId ? { requiredRevisionId: revisionId } : undefined);
|
|
45
|
+
trackMutation(args.documentId, writeResponse?.writeControl?.requiredRevisionId);
|
|
43
46
|
const docUrl = `https://docs.google.com/document/d/${args.documentId}/edit`;
|
|
44
47
|
return `${docUrl}\nSuccessfully inserted a ${args.rows}x${args.columns} table at index ${args.index}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
|
|
45
48
|
}
|
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { getDocsClient } from '../../clients.js';
|
|
4
4
|
import { DocumentIdParameter } from '../../types.js';
|
|
5
5
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
+
import { getLastReadRevisionId, trackMutation } from '../../readTracker.js';
|
|
6
7
|
// --- Table Index Math ---
|
|
7
8
|
// Google Docs API table index layout for an R×C table inserted at T:
|
|
8
9
|
// cellContentIndex(T, r, c, C) = T + 4 + r * (1 + 2*C) + 2*c
|
|
@@ -118,7 +119,9 @@ export function register(server) {
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
const requests = buildInsertTableWithDataRequests(args.data, args.index, args.hasHeaderRow ?? false, args.tabId);
|
|
121
|
-
const
|
|
122
|
+
const revisionId = getLastReadRevisionId(args.documentId);
|
|
123
|
+
const metadata = await GDocsHelpers.executeBatchUpdateWithSplitting(docs, args.documentId, requests, log, revisionId ? { requiredRevisionId: revisionId } : undefined);
|
|
124
|
+
trackMutation(args.documentId, metadata?.finalWriteControl?.requiredRevisionId);
|
|
122
125
|
const docUrl = `https://docs.google.com/document/d/${args.documentId}/edit`;
|
|
123
126
|
return (`${docUrl}\nSuccessfully inserted a ${numRows}x${numCols} table with data at index ${args.index}` +
|
|
124
127
|
`${args.tabId ? ` in tab ${args.tabId}` : ''}. ` +
|