google-tools-mcp 1.2.11 → 1.2.12
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/README.md +18 -0
- package/dist/markdown-transformer/docsToMarkdown.js +94 -21
- package/dist/markdown-transformer/index.js +2 -2
- package/dist/markdown-transformer/markdownToDocs.js +295 -23
- package/dist/tools/drive/createDocument.js +1 -1
- package/dist/tools/extras/readDriveFile.js +15 -10
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +1 -1
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -109,6 +109,24 @@ Add the credentials directly to your MCP configuration:
|
|
|
109
109
|
|
|
110
110
|
### Step 3: Add to Your MCP Client
|
|
111
111
|
|
|
112
|
+
#### Local unpublished checkout
|
|
113
|
+
|
|
114
|
+
To call a local tool directly without publishing or using MCP transport, use
|
|
115
|
+
`local:tool`. This loads the unpublished checkout, registers the real tools in
|
|
116
|
+
memory, validates the arguments, and calls the tool's `execute` function:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm run local:tool -- list
|
|
120
|
+
npm run local:tool -- help
|
|
121
|
+
npm run local:tool -- readDocument documentId=... format=markdown
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
For larger arguments, put JSON in a file and pass it with `@`:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm run local:tool -- replaceDocumentWithMarkdown @args.json
|
|
128
|
+
```
|
|
129
|
+
|
|
112
130
|
#### Codex
|
|
113
131
|
|
|
114
132
|
```bash
|
|
@@ -15,34 +15,43 @@ const CODE_FONT_FAMILIES = new Set(['Roboto Mono', 'Courier New', 'Consolas', 'm
|
|
|
15
15
|
* underline, links, code), ordered & unordered lists with nesting, tables,
|
|
16
16
|
* and section breaks.
|
|
17
17
|
*/
|
|
18
|
-
export function docsJsonToMarkdown(docData) {
|
|
18
|
+
export function docsJsonToMarkdown(docData, options = {}) {
|
|
19
19
|
const body = docData.body;
|
|
20
20
|
if (!body?.content) {
|
|
21
21
|
return '';
|
|
22
22
|
}
|
|
23
23
|
const lists = docData.lists ?? {};
|
|
24
|
+
const conversionOptions = {
|
|
25
|
+
richMarkdown: options.plainMarkdown ? false : options.richMarkdown ?? true,
|
|
26
|
+
};
|
|
24
27
|
let markdown = '';
|
|
25
28
|
for (const element of body.content) {
|
|
26
29
|
if (element.paragraph) {
|
|
27
|
-
markdown += convertParagraph(element.paragraph, lists);
|
|
30
|
+
markdown += convertParagraph(element.paragraph, lists, conversionOptions);
|
|
28
31
|
}
|
|
29
32
|
else if (element.table) {
|
|
30
|
-
markdown += convertTable(element.table);
|
|
33
|
+
markdown += convertTable(element.table, conversionOptions);
|
|
31
34
|
}
|
|
32
35
|
else if (element.sectionBreak) {
|
|
36
|
+
if (isInitialDocumentSectionBreak(element)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
33
39
|
markdown += '\n---\n\n';
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
return markdown.trim();
|
|
37
43
|
}
|
|
44
|
+
function isInitialDocumentSectionBreak(element) {
|
|
45
|
+
return element.endIndex === 1 && element.startIndex === undefined;
|
|
46
|
+
}
|
|
38
47
|
// --- Paragraph Conversion ---
|
|
39
|
-
function convertParagraph(paragraph, lists) {
|
|
48
|
+
function convertParagraph(paragraph, lists, options) {
|
|
40
49
|
// 1. Determine paragraph type
|
|
41
50
|
const headingLevel = getHeadingLevel(paragraph);
|
|
42
51
|
const listInfo = getListInfo(paragraph, lists);
|
|
43
52
|
// 2. Extract text content with inline formatting
|
|
44
53
|
const elements = paragraph.elements ?? [];
|
|
45
|
-
const text = extractFormattedText(elements);
|
|
54
|
+
const text = extractFormattedText(elements, options);
|
|
46
55
|
// 3. Format based on type
|
|
47
56
|
if (headingLevel && text.trim()) {
|
|
48
57
|
const hashes = '#'.repeat(Math.min(headingLevel, 6));
|
|
@@ -54,7 +63,15 @@ function convertParagraph(paragraph, lists) {
|
|
|
54
63
|
return `${indent}${marker} ${text.trim()}\n`;
|
|
55
64
|
}
|
|
56
65
|
if (text.trim()) {
|
|
57
|
-
|
|
66
|
+
const trimmed = text.trim();
|
|
67
|
+
if (options.richMarkdown && isBlockquoteParagraph(paragraph)) {
|
|
68
|
+
return `<blockquote>${trimmed}</blockquote>\n\n`;
|
|
69
|
+
}
|
|
70
|
+
const alignment = paragraphAlignmentToHtml(paragraph.paragraphStyle?.alignment);
|
|
71
|
+
if (options.richMarkdown && alignment) {
|
|
72
|
+
return `<p align="${alignment}">${trimmed}</p>\n\n`;
|
|
73
|
+
}
|
|
74
|
+
return `${trimmed}\n\n`;
|
|
58
75
|
}
|
|
59
76
|
return '\n';
|
|
60
77
|
}
|
|
@@ -91,16 +108,16 @@ function getListInfo(paragraph, lists) {
|
|
|
91
108
|
return { ordered, nestingLevel };
|
|
92
109
|
}
|
|
93
110
|
// --- Text Run Conversion ---
|
|
94
|
-
function extractFormattedText(elements) {
|
|
111
|
+
function extractFormattedText(elements, options) {
|
|
95
112
|
let result = '';
|
|
96
113
|
for (const element of elements) {
|
|
97
114
|
if (element.textRun) {
|
|
98
|
-
result += convertTextRun(element.textRun);
|
|
115
|
+
result += convertTextRun(element.textRun, options);
|
|
99
116
|
}
|
|
100
117
|
}
|
|
101
118
|
return result;
|
|
102
119
|
}
|
|
103
|
-
function convertTextRun(textRun) {
|
|
120
|
+
function convertTextRun(textRun, options) {
|
|
104
121
|
let text = textRun.content ?? '';
|
|
105
122
|
const style = textRun.textStyle;
|
|
106
123
|
if (!style)
|
|
@@ -134,20 +151,72 @@ function convertTextRun(textRun) {
|
|
|
134
151
|
if (style.strikethrough) {
|
|
135
152
|
formatted = `~~${formatted}~~`;
|
|
136
153
|
}
|
|
137
|
-
if (style.underline && !style.link) {
|
|
154
|
+
if (options.richMarkdown && style.underline && !style.link) {
|
|
138
155
|
formatted = `<u>${formatted}</u>`;
|
|
139
156
|
}
|
|
157
|
+
if (options.richMarkdown) {
|
|
158
|
+
formatted = applyRichTextStyle(formatted, style);
|
|
159
|
+
}
|
|
140
160
|
if (style.link?.url) {
|
|
141
161
|
formatted = `[${formatted}](${style.link.url})`;
|
|
142
162
|
}
|
|
143
163
|
return formatted + (trailingNewline ? '\n' : '');
|
|
144
164
|
}
|
|
165
|
+
function applyRichTextStyle(text, style) {
|
|
166
|
+
const styles = [];
|
|
167
|
+
const fg = rgbColorToHex(style.foregroundColor?.color?.rgbColor);
|
|
168
|
+
const bg = rgbColorToHex(style.backgroundColor?.color?.rgbColor);
|
|
169
|
+
const fontSize = style.fontSize?.magnitude;
|
|
170
|
+
const fontFamily = style.weightedFontFamily?.fontFamily;
|
|
171
|
+
if (fg)
|
|
172
|
+
styles.push(`color:${fg}`);
|
|
173
|
+
if (bg)
|
|
174
|
+
styles.push(`background-color:${bg}`);
|
|
175
|
+
if (typeof fontSize === 'number')
|
|
176
|
+
styles.push(`font-size:${fontSize}pt`);
|
|
177
|
+
if (fontFamily && !CODE_FONT_FAMILIES.has(fontFamily))
|
|
178
|
+
styles.push(`font-family:${escapeHtmlAttr(fontFamily)}`);
|
|
179
|
+
if (styles.length === 0)
|
|
180
|
+
return text;
|
|
181
|
+
return `<span style="${styles.join(';')}">${text}</span>`;
|
|
182
|
+
}
|
|
145
183
|
function isCodeStyled(style) {
|
|
146
184
|
const fontFamily = style.weightedFontFamily?.fontFamily;
|
|
147
185
|
return typeof fontFamily === 'string' && CODE_FONT_FAMILIES.has(fontFamily);
|
|
148
186
|
}
|
|
187
|
+
function isBlockquoteParagraph(paragraph) {
|
|
188
|
+
const style = paragraph.paragraphStyle;
|
|
189
|
+
return Boolean(style?.borderLeft ||
|
|
190
|
+
style?.indentStart?.magnitude >= 30 ||
|
|
191
|
+
style?.indentFirstLine?.magnitude >= 30);
|
|
192
|
+
}
|
|
193
|
+
function paragraphAlignmentToHtml(alignment) {
|
|
194
|
+
switch (alignment) {
|
|
195
|
+
case 'CENTER':
|
|
196
|
+
return 'center';
|
|
197
|
+
case 'END':
|
|
198
|
+
case 'RIGHT':
|
|
199
|
+
return 'right';
|
|
200
|
+
case 'JUSTIFIED':
|
|
201
|
+
return 'justify';
|
|
202
|
+
default:
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function rgbColorToHex(rgb) {
|
|
207
|
+
if (!rgb)
|
|
208
|
+
return null;
|
|
209
|
+
const toHex = (value) => {
|
|
210
|
+
const normalized = Math.max(0, Math.min(255, Math.round((value ?? 0) * 255)));
|
|
211
|
+
return normalized.toString(16).padStart(2, '0');
|
|
212
|
+
};
|
|
213
|
+
return `#${toHex(rgb.red)}${toHex(rgb.green)}${toHex(rgb.blue)}`;
|
|
214
|
+
}
|
|
215
|
+
function escapeHtmlAttr(value) {
|
|
216
|
+
return value.replace(/&/g, '&').replace(/"/g, '"');
|
|
217
|
+
}
|
|
149
218
|
// --- Table Conversion ---
|
|
150
|
-
function convertTable(table) {
|
|
219
|
+
function convertTable(table, options) {
|
|
151
220
|
if (!table.tableRows || table.tableRows.length === 0) {
|
|
152
221
|
return '';
|
|
153
222
|
}
|
|
@@ -162,15 +231,15 @@ function convertTable(table) {
|
|
|
162
231
|
continue;
|
|
163
232
|
let rowText = '|';
|
|
164
233
|
for (const cell of row.tableCells) {
|
|
165
|
-
const cellText = extractCellText(cell);
|
|
234
|
+
const cellText = extractCellText(cell, options);
|
|
166
235
|
rowText += ` ${cellText} |`;
|
|
167
236
|
}
|
|
168
237
|
markdown += rowText + '\n';
|
|
169
238
|
// Add header separator after the first row
|
|
170
239
|
if (isFirstRow) {
|
|
171
240
|
let separator = '|';
|
|
172
|
-
for (
|
|
173
|
-
separator +=
|
|
241
|
+
for (const cell of row.tableCells) {
|
|
242
|
+
separator += ` ${tableAlignmentMarker(cell)} |`;
|
|
174
243
|
}
|
|
175
244
|
markdown += separator + '\n';
|
|
176
245
|
isFirstRow = false;
|
|
@@ -242,18 +311,22 @@ function convertCodeBlockTable(table) {
|
|
|
242
311
|
}
|
|
243
312
|
return '\n```\n' + codeText + '\n```\n\n';
|
|
244
313
|
}
|
|
245
|
-
function
|
|
314
|
+
function tableAlignmentMarker(cell) {
|
|
315
|
+
const alignment = paragraphAlignmentToHtml(cell.content?.[0]?.paragraph?.paragraphStyle?.alignment);
|
|
316
|
+
if (alignment === 'center')
|
|
317
|
+
return ':---:';
|
|
318
|
+
if (alignment === 'right')
|
|
319
|
+
return '---:';
|
|
320
|
+
return '---';
|
|
321
|
+
}
|
|
322
|
+
function extractCellText(cell, options) {
|
|
246
323
|
let text = '';
|
|
247
324
|
if (!cell.content)
|
|
248
325
|
return text;
|
|
249
326
|
for (const element of cell.content) {
|
|
250
327
|
if (element.paragraph?.elements) {
|
|
251
|
-
|
|
252
|
-
if (pe.textRun?.content) {
|
|
253
|
-
text += pe.textRun.content.replace(/\n/g, ' ').trim();
|
|
254
|
-
}
|
|
255
|
-
}
|
|
328
|
+
text += extractFormattedText(element.paragraph.elements, options).replace(/\n/g, ' ');
|
|
256
329
|
}
|
|
257
330
|
}
|
|
258
|
-
return text;
|
|
331
|
+
return text.trim().replace(/\|/g, '\\|');
|
|
259
332
|
}
|
|
@@ -61,12 +61,12 @@ export async function extractMarkdown(docs, documentId, options) {
|
|
|
61
61
|
return docsJsonToMarkdown({
|
|
62
62
|
body: targetTab.documentTab.body,
|
|
63
63
|
lists: targetTab.documentTab.lists,
|
|
64
|
-
});
|
|
64
|
+
}, options);
|
|
65
65
|
}
|
|
66
66
|
return docsJsonToMarkdown({
|
|
67
67
|
body: res.data.body,
|
|
68
68
|
lists: res.data.lists,
|
|
69
|
-
});
|
|
69
|
+
}, options);
|
|
70
70
|
}
|
|
71
71
|
// --- insertMarkdown ---
|
|
72
72
|
/**
|
|
@@ -4,7 +4,7 @@ import { MarkdownConversionError } from '../types.js';
|
|
|
4
4
|
// --- Markdown-it Setup ---
|
|
5
5
|
function createParser() {
|
|
6
6
|
return new MarkdownIt({
|
|
7
|
-
html:
|
|
7
|
+
html: true,
|
|
8
8
|
linkify: true,
|
|
9
9
|
typographer: false,
|
|
10
10
|
breaks: false,
|
|
@@ -23,6 +23,79 @@ function getHeadingLevel(token) {
|
|
|
23
23
|
const match = token.tag.match(/h(\d)/);
|
|
24
24
|
return match ? parseInt(match[1], 10) : null;
|
|
25
25
|
}
|
|
26
|
+
function getAttr(token, name) {
|
|
27
|
+
return token.attrs?.find((attr) => attr[0].toLowerCase() === name.toLowerCase())?.[1] ?? null;
|
|
28
|
+
}
|
|
29
|
+
function parseAlignmentFromStyle(style) {
|
|
30
|
+
const match = style?.match(/(?:^|;)\s*text-align\s*:\s*(left|center|right|justify)\s*(?:;|$)/i);
|
|
31
|
+
if (!match)
|
|
32
|
+
return null;
|
|
33
|
+
return alignmentToDocs(match[1]);
|
|
34
|
+
}
|
|
35
|
+
function alignmentToDocs(value) {
|
|
36
|
+
switch (value.toLowerCase()) {
|
|
37
|
+
case 'center':
|
|
38
|
+
return 'CENTER';
|
|
39
|
+
case 'right':
|
|
40
|
+
return 'END';
|
|
41
|
+
case 'justify':
|
|
42
|
+
case 'justified':
|
|
43
|
+
return 'JUSTIFIED';
|
|
44
|
+
case 'left':
|
|
45
|
+
return 'START';
|
|
46
|
+
default:
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function parseStyleDeclarations(style) {
|
|
51
|
+
const formatting = {};
|
|
52
|
+
if (!style)
|
|
53
|
+
return formatting;
|
|
54
|
+
for (const declaration of style.split(';')) {
|
|
55
|
+
const [rawName, ...rawValueParts] = declaration.split(':');
|
|
56
|
+
if (!rawName || rawValueParts.length === 0)
|
|
57
|
+
continue;
|
|
58
|
+
const name = rawName.trim().toLowerCase();
|
|
59
|
+
const value = rawValueParts.join(':').trim();
|
|
60
|
+
if ((name === 'color' || name === 'foreground-color') && /^#[0-9a-f]{6}$/i.test(value)) {
|
|
61
|
+
formatting.foregroundColor = value;
|
|
62
|
+
}
|
|
63
|
+
else if ((name === 'background-color' || name === 'background') && /^#[0-9a-f]{6}$/i.test(value)) {
|
|
64
|
+
formatting.backgroundColor = value;
|
|
65
|
+
}
|
|
66
|
+
else if (name === 'font-size') {
|
|
67
|
+
const match = value.match(/^(\d+(?:\.\d+)?)pt$/i);
|
|
68
|
+
if (match)
|
|
69
|
+
formatting.fontSize = Number(match[1]);
|
|
70
|
+
}
|
|
71
|
+
else if (name === 'font-family') {
|
|
72
|
+
const family = value.split(',')[0]?.trim().replace(/^['"]|['"]$/g, '');
|
|
73
|
+
if (family && /^[\w .-]+$/.test(family))
|
|
74
|
+
formatting.fontFamily = family;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return formatting;
|
|
78
|
+
}
|
|
79
|
+
function parseRichHtmlTag(rawHtml) {
|
|
80
|
+
const trimmed = rawHtml.trim();
|
|
81
|
+
const match = trimmed.match(/^<\/?\s*([a-z0-9]+)([^>]*)\/?>$/i);
|
|
82
|
+
if (!match)
|
|
83
|
+
return null;
|
|
84
|
+
const tag = match[1].toLowerCase();
|
|
85
|
+
const attrs = match[2] ?? '';
|
|
86
|
+
const closing = /^<\s*\//.test(trimmed);
|
|
87
|
+
const selfClosing = /\/\s*>$/.test(trimmed);
|
|
88
|
+
return { tag, attrs, closing, selfClosing };
|
|
89
|
+
}
|
|
90
|
+
function parseAttrs(rawAttrs) {
|
|
91
|
+
const attrs = {};
|
|
92
|
+
const attrRegex = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/g;
|
|
93
|
+
let match;
|
|
94
|
+
while ((match = attrRegex.exec(rawAttrs)) !== null) {
|
|
95
|
+
attrs[match[1].toLowerCase()] = match[2] ?? match[3] ?? match[4] ?? '';
|
|
96
|
+
}
|
|
97
|
+
return attrs;
|
|
98
|
+
}
|
|
26
99
|
const CODE_FONT_FAMILY = 'Roboto Mono';
|
|
27
100
|
const CODE_TEXT_HEX = '#188038';
|
|
28
101
|
const CODE_BACKGROUND_HEX = '#F1F3F4';
|
|
@@ -86,6 +159,8 @@ export function convertMarkdownToRequests(markdown, startIndex = 1, tabId, optio
|
|
|
86
159
|
codeBlockRanges: [],
|
|
87
160
|
tableState: undefined,
|
|
88
161
|
inTableCell: false,
|
|
162
|
+
paragraphFormattingStack: [],
|
|
163
|
+
htmlParagraphPushStack: [],
|
|
89
164
|
tabId,
|
|
90
165
|
titleConsumed: false,
|
|
91
166
|
firstHeadingAsTitle: options?.firstHeadingAsTitle ?? false,
|
|
@@ -231,10 +306,12 @@ function processToken(token, context) {
|
|
|
231
306
|
case 'th_open':
|
|
232
307
|
case 'td_open': {
|
|
233
308
|
if (context.tableState) {
|
|
309
|
+
const alignment = parseAlignmentFromStyle(getAttr(token, 'style'));
|
|
234
310
|
context.tableState.currentCell = {
|
|
235
311
|
text: '',
|
|
236
312
|
isHeader: context.tableState.inHeader || token.type === 'th_open',
|
|
237
313
|
textRanges: [],
|
|
314
|
+
alignment,
|
|
238
315
|
};
|
|
239
316
|
context.inTableCell = true;
|
|
240
317
|
}
|
|
@@ -264,9 +341,20 @@ function processToken(token, context) {
|
|
|
264
341
|
case 'hr':
|
|
265
342
|
handleHorizontalRule(context);
|
|
266
343
|
break;
|
|
267
|
-
// Blockquotes (skip for now)
|
|
268
344
|
case 'blockquote_open':
|
|
345
|
+
context.paragraphFormattingStack.push({
|
|
346
|
+
indentStart: 36,
|
|
347
|
+
borderLeft: true,
|
|
348
|
+
});
|
|
349
|
+
break;
|
|
269
350
|
case 'blockquote_close':
|
|
351
|
+
context.paragraphFormattingStack.pop();
|
|
352
|
+
break;
|
|
353
|
+
case 'html_inline':
|
|
354
|
+
handleHtmlInlineToken(token, context);
|
|
355
|
+
break;
|
|
356
|
+
case 'html_block':
|
|
357
|
+
handleHtmlBlockToken(token, context);
|
|
270
358
|
break;
|
|
271
359
|
default:
|
|
272
360
|
break;
|
|
@@ -306,6 +394,106 @@ function handleHorizontalRule(context) {
|
|
|
306
394
|
insertText('\n', context);
|
|
307
395
|
context.hrRanges.push({ startIndex: start, endIndex: context.currentIndex });
|
|
308
396
|
}
|
|
397
|
+
function handleHtmlInlineToken(token, context) {
|
|
398
|
+
const parsed = parseRichHtmlTag(token.content);
|
|
399
|
+
if (!parsed)
|
|
400
|
+
return;
|
|
401
|
+
const attrs = parseAttrs(parsed.attrs);
|
|
402
|
+
if (parsed.selfClosing && parsed.tag !== 'br')
|
|
403
|
+
return;
|
|
404
|
+
if (parsed.closing) {
|
|
405
|
+
switch (parsed.tag) {
|
|
406
|
+
case 'u':
|
|
407
|
+
popFormatting(context, 'underline');
|
|
408
|
+
break;
|
|
409
|
+
case 'mark':
|
|
410
|
+
popFormatting(context, 'backgroundColor');
|
|
411
|
+
break;
|
|
412
|
+
case 'span':
|
|
413
|
+
popFormatting(context, 'richSpan');
|
|
414
|
+
break;
|
|
415
|
+
case 'p':
|
|
416
|
+
case 'div': {
|
|
417
|
+
const pushed = context.htmlParagraphPushStack.pop();
|
|
418
|
+
if (pushed)
|
|
419
|
+
context.paragraphFormattingStack.pop();
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case 'blockquote':
|
|
423
|
+
context.paragraphFormattingStack.pop();
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
switch (parsed.tag) {
|
|
429
|
+
case 'u':
|
|
430
|
+
context.formattingStack.push({ underline: true });
|
|
431
|
+
break;
|
|
432
|
+
case 'mark':
|
|
433
|
+
context.formattingStack.push({ backgroundColor: '#FFF2CC' });
|
|
434
|
+
break;
|
|
435
|
+
case 'span': {
|
|
436
|
+
const formatting = parseStyleDeclarations(attrs.style);
|
|
437
|
+
if (hasFormatting(formatting)) {
|
|
438
|
+
formatting.richSpan = true;
|
|
439
|
+
context.formattingStack.push(formatting);
|
|
440
|
+
}
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
case 'p':
|
|
444
|
+
case 'div': {
|
|
445
|
+
const alignment = alignmentToDocs(attrs.align ?? '') ?? parseAlignmentFromStyle(attrs.style);
|
|
446
|
+
if (alignment) {
|
|
447
|
+
context.paragraphFormattingStack.push({ alignment });
|
|
448
|
+
context.htmlParagraphPushStack.push(true);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
context.htmlParagraphPushStack.push(false);
|
|
452
|
+
}
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
case 'blockquote':
|
|
456
|
+
context.paragraphFormattingStack.push({
|
|
457
|
+
indentStart: 36,
|
|
458
|
+
borderLeft: true,
|
|
459
|
+
});
|
|
460
|
+
break;
|
|
461
|
+
case 'br':
|
|
462
|
+
insertText('\n', context);
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function handleHtmlBlockToken(token, context) {
|
|
467
|
+
const content = token.content.trim();
|
|
468
|
+
const match = content.match(/^<\s*(p|div|blockquote)\b([^>]*)>([\s\S]*)<\/\s*\1\s*>$/i);
|
|
469
|
+
if (!match)
|
|
470
|
+
return;
|
|
471
|
+
const tag = match[1].toLowerCase();
|
|
472
|
+
const attrs = parseAttrs(match[2]);
|
|
473
|
+
const inner = match[3];
|
|
474
|
+
let pushedParagraphFormatting = false;
|
|
475
|
+
if (tag === 'blockquote') {
|
|
476
|
+
context.paragraphFormattingStack.push({
|
|
477
|
+
indentStart: 36,
|
|
478
|
+
borderLeft: true,
|
|
479
|
+
});
|
|
480
|
+
pushedParagraphFormatting = true;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
const alignment = alignmentToDocs(attrs.align ?? '') ?? parseAlignmentFromStyle(attrs.style);
|
|
484
|
+
if (alignment) {
|
|
485
|
+
context.paragraphFormattingStack.push({ alignment });
|
|
486
|
+
pushedParagraphFormatting = true;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const parser = createParser();
|
|
490
|
+
for (const child of parser.parse(inner, {})) {
|
|
491
|
+
processToken(child, context);
|
|
492
|
+
}
|
|
493
|
+
if (pushedParagraphFormatting) {
|
|
494
|
+
context.paragraphFormattingStack.pop();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
309
497
|
// --- Paragraph Handlers ---
|
|
310
498
|
function handleParagraphOpen(context) {
|
|
311
499
|
if (context.listStack.length === 0) {
|
|
@@ -329,10 +517,15 @@ function handleParagraphClose(context) {
|
|
|
329
517
|
}
|
|
330
518
|
// Record the range for normal paragraphs (not list items) so we can apply spacing later
|
|
331
519
|
if (paragraphStart !== undefined && context.listStack.length === 0) {
|
|
332
|
-
|
|
520
|
+
const paragraphRange = {
|
|
333
521
|
startIndex: paragraphStart,
|
|
334
522
|
endIndex: context.currentIndex,
|
|
335
|
-
}
|
|
523
|
+
};
|
|
524
|
+
const paragraphFormatting = mergeParagraphFormattingStack(context.paragraphFormattingStack);
|
|
525
|
+
if (hasParagraphFormatting(paragraphFormatting)) {
|
|
526
|
+
paragraphRange.formatting = paragraphFormatting;
|
|
527
|
+
}
|
|
528
|
+
context.normalParagraphRanges.push(paragraphRange);
|
|
336
529
|
}
|
|
337
530
|
context.currentParagraphStart = undefined;
|
|
338
531
|
}
|
|
@@ -548,15 +741,13 @@ function handleTableClose(tableState, context) {
|
|
|
548
741
|
if (range.formatting.bold ||
|
|
549
742
|
range.formatting.italic ||
|
|
550
743
|
range.formatting.strikethrough ||
|
|
551
|
-
range.formatting.code
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
backgroundColor: range.formatting.code ? CODE_BACKGROUND_HEX : undefined,
|
|
559
|
-
}, context.tabId);
|
|
744
|
+
range.formatting.code ||
|
|
745
|
+
range.formatting.underline ||
|
|
746
|
+
range.formatting.foregroundColor ||
|
|
747
|
+
range.formatting.backgroundColor ||
|
|
748
|
+
range.formatting.fontSize ||
|
|
749
|
+
range.formatting.fontFamily) {
|
|
750
|
+
const styleReq = buildUpdateTextStyleRequest(absStart, absEnd, formattingToTextStyle(range.formatting), context.tabId);
|
|
560
751
|
if (styleReq)
|
|
561
752
|
context.formatRequests.push(styleReq.request);
|
|
562
753
|
}
|
|
@@ -572,6 +763,11 @@ function handleTableClose(tableState, context) {
|
|
|
572
763
|
if (headerStyleReq)
|
|
573
764
|
context.formatRequests.push(headerStyleReq.request);
|
|
574
765
|
}
|
|
766
|
+
if (cell.alignment) {
|
|
767
|
+
const alignmentReq = buildUpdateParagraphStyleRequest(adjustedIndex, adjustedIndex + cell.text.length, { alignment: cell.alignment }, context.tabId);
|
|
768
|
+
if (alignmentReq)
|
|
769
|
+
context.formatRequests.push(alignmentReq.request);
|
|
770
|
+
}
|
|
575
771
|
cumulativeTextLength += cell.text.length;
|
|
576
772
|
}
|
|
577
773
|
}
|
|
@@ -606,6 +802,18 @@ function mergeFormattingStack(stack) {
|
|
|
606
802
|
merged.code = state.code;
|
|
607
803
|
if (state.link !== undefined)
|
|
608
804
|
merged.link = state.link;
|
|
805
|
+
if (state.underline !== undefined)
|
|
806
|
+
merged.underline = state.underline;
|
|
807
|
+
if (state.foregroundColor !== undefined)
|
|
808
|
+
merged.foregroundColor = state.foregroundColor;
|
|
809
|
+
if (state.backgroundColor !== undefined)
|
|
810
|
+
merged.backgroundColor = state.backgroundColor;
|
|
811
|
+
if (state.fontSize !== undefined)
|
|
812
|
+
merged.fontSize = state.fontSize;
|
|
813
|
+
if (state.fontFamily !== undefined)
|
|
814
|
+
merged.fontFamily = state.fontFamily;
|
|
815
|
+
if (state.richSpan !== undefined)
|
|
816
|
+
merged.richSpan = state.richSpan;
|
|
609
817
|
}
|
|
610
818
|
return merged;
|
|
611
819
|
}
|
|
@@ -614,7 +822,41 @@ function hasFormatting(formatting) {
|
|
|
614
822
|
formatting.italic === true ||
|
|
615
823
|
formatting.strikethrough === true ||
|
|
616
824
|
formatting.code === true ||
|
|
617
|
-
formatting.link !== undefined
|
|
825
|
+
formatting.link !== undefined ||
|
|
826
|
+
formatting.underline === true ||
|
|
827
|
+
formatting.foregroundColor !== undefined ||
|
|
828
|
+
formatting.backgroundColor !== undefined ||
|
|
829
|
+
formatting.fontSize !== undefined ||
|
|
830
|
+
formatting.fontFamily !== undefined);
|
|
831
|
+
}
|
|
832
|
+
function formattingToTextStyle(formatting) {
|
|
833
|
+
return {
|
|
834
|
+
bold: formatting.bold,
|
|
835
|
+
italic: formatting.italic,
|
|
836
|
+
underline: formatting.underline,
|
|
837
|
+
strikethrough: formatting.strikethrough,
|
|
838
|
+
fontFamily: formatting.code ? CODE_FONT_FAMILY : formatting.fontFamily,
|
|
839
|
+
fontSize: formatting.fontSize,
|
|
840
|
+
foregroundColor: formatting.code ? CODE_TEXT_HEX : formatting.foregroundColor,
|
|
841
|
+
backgroundColor: formatting.code ? CODE_BACKGROUND_HEX : formatting.backgroundColor,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function mergeParagraphFormattingStack(stack) {
|
|
845
|
+
const merged = {};
|
|
846
|
+
for (const state of stack) {
|
|
847
|
+
if (state.alignment !== undefined)
|
|
848
|
+
merged.alignment = state.alignment;
|
|
849
|
+
if (state.indentStart !== undefined)
|
|
850
|
+
merged.indentStart = state.indentStart;
|
|
851
|
+
if (state.borderLeft !== undefined)
|
|
852
|
+
merged.borderLeft = state.borderLeft;
|
|
853
|
+
}
|
|
854
|
+
return merged;
|
|
855
|
+
}
|
|
856
|
+
function hasParagraphFormatting(formatting) {
|
|
857
|
+
return (formatting.alignment !== undefined ||
|
|
858
|
+
formatting.indentStart !== undefined ||
|
|
859
|
+
formatting.borderLeft === true);
|
|
618
860
|
}
|
|
619
861
|
function popFormatting(context, type) {
|
|
620
862
|
for (let i = context.formattingStack.length - 1; i >= 0; i--) {
|
|
@@ -661,15 +903,13 @@ function finalizeFormatting(context) {
|
|
|
661
903
|
if (range.formatting.bold ||
|
|
662
904
|
range.formatting.italic ||
|
|
663
905
|
range.formatting.strikethrough ||
|
|
664
|
-
range.formatting.code
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
backgroundColor: range.formatting.code ? CODE_BACKGROUND_HEX : undefined,
|
|
672
|
-
}, context.tabId);
|
|
906
|
+
range.formatting.code ||
|
|
907
|
+
range.formatting.underline ||
|
|
908
|
+
range.formatting.foregroundColor ||
|
|
909
|
+
range.formatting.backgroundColor ||
|
|
910
|
+
range.formatting.fontSize ||
|
|
911
|
+
range.formatting.fontFamily) {
|
|
912
|
+
const styleRequest = buildUpdateTextStyleRequest(range.startIndex, range.endIndex, formattingToTextStyle(range.formatting), context.tabId);
|
|
673
913
|
if (styleRequest) {
|
|
674
914
|
context.formatRequests.push(styleRequest.request);
|
|
675
915
|
}
|
|
@@ -711,6 +951,38 @@ function finalizeFormatting(context) {
|
|
|
711
951
|
fields: 'spaceBelow',
|
|
712
952
|
},
|
|
713
953
|
});
|
|
954
|
+
if (normalRange.formatting) {
|
|
955
|
+
const paraStyle = {};
|
|
956
|
+
const fields = [];
|
|
957
|
+
if (normalRange.formatting.alignment) {
|
|
958
|
+
paraStyle.alignment = normalRange.formatting.alignment;
|
|
959
|
+
fields.push('alignment');
|
|
960
|
+
}
|
|
961
|
+
if (normalRange.formatting.indentStart !== undefined) {
|
|
962
|
+
paraStyle.indentStart = { magnitude: normalRange.formatting.indentStart, unit: 'PT' };
|
|
963
|
+
fields.push('indentStart');
|
|
964
|
+
}
|
|
965
|
+
if (normalRange.formatting.borderLeft) {
|
|
966
|
+
paraStyle.borderLeft = {
|
|
967
|
+
color: {
|
|
968
|
+
color: { rgbColor: { red: 0.75, green: 0.75, blue: 0.75 } },
|
|
969
|
+
},
|
|
970
|
+
width: { magnitude: 2, unit: 'PT' },
|
|
971
|
+
padding: { magnitude: 6, unit: 'PT' },
|
|
972
|
+
dashStyle: 'SOLID',
|
|
973
|
+
};
|
|
974
|
+
fields.push('borderLeft');
|
|
975
|
+
}
|
|
976
|
+
if (fields.length > 0) {
|
|
977
|
+
context.formatRequests.push({
|
|
978
|
+
updateParagraphStyle: {
|
|
979
|
+
range,
|
|
980
|
+
paragraphStyle: paraStyle,
|
|
981
|
+
fields: fields.join(','),
|
|
982
|
+
},
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
714
986
|
}
|
|
715
987
|
// List trailing spacing: apply spaceBelow to the last paragraph of each
|
|
716
988
|
// top-level list so there is a visible gap between the list and the content
|
|
@@ -15,7 +15,7 @@ export function register(server) {
|
|
|
15
15
|
initialContent: z
|
|
16
16
|
.string()
|
|
17
17
|
.optional()
|
|
18
|
-
.describe('Initial content to add to the document. By default, markdown syntax is converted to formatted Google Docs content (headings, bold, italic, links, lists,
|
|
18
|
+
.describe('Initial content to add to the document. By default, markdown syntax is converted to formatted Google Docs content (headings, bold, italic, links, tables, lists, and rich markdown HTML extensions).'),
|
|
19
19
|
contentFormat: z
|
|
20
20
|
.enum(['markdown', 'raw'])
|
|
21
21
|
.optional()
|
|
@@ -49,14 +49,19 @@ export function register(server) {
|
|
|
49
49
|
fileId: z
|
|
50
50
|
.string()
|
|
51
51
|
.describe('The Google Drive file ID (the long string between /d/ and /edit in a Drive URL).'),
|
|
52
|
-
format: z
|
|
53
|
-
.enum(['text', 'json', 'markdown'])
|
|
54
|
-
.optional()
|
|
55
|
-
.default('markdown')
|
|
56
|
-
.describe("Output format for Google Docs: 'markdown' (default), 'text' (plain text), 'json' (raw structure). Ignored for non-Doc files."),
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
.optional()
|
|
52
|
+
format: z
|
|
53
|
+
.enum(['text', 'json', 'markdown'])
|
|
54
|
+
.optional()
|
|
55
|
+
.default('markdown')
|
|
56
|
+
.describe("Output format for Google Docs: 'markdown' (default, preserves rich text/paragraph formatting with a small HTML allowlist), 'text' (plain text), 'json' (raw structure). Ignored for non-Doc files."),
|
|
57
|
+
plainMarkdown: z
|
|
58
|
+
.boolean()
|
|
59
|
+
.optional()
|
|
60
|
+
.default(false)
|
|
61
|
+
.describe('For Google Docs markdown output only. If true, suppresses rich HTML-style formatting extensions and returns cleaner portable markdown.'),
|
|
62
|
+
range: z
|
|
63
|
+
.string()
|
|
64
|
+
.optional()
|
|
60
65
|
.describe("A1 notation range for Google Sheets (e.g., 'Sheet1!A1:C10'). Required for spreadsheets."),
|
|
61
66
|
}),
|
|
62
67
|
execute: async (args, { log }) => {
|
|
@@ -117,8 +122,8 @@ async function readGoogleDoc(args, fileMeta, log) {
|
|
|
117
122
|
let content;
|
|
118
123
|
if (args.format === 'json') {
|
|
119
124
|
content = JSON.stringify(res.data, null, 2);
|
|
120
|
-
} else if (args.format === 'markdown') {
|
|
121
|
-
content = docsJsonToMarkdown(res.data);
|
|
125
|
+
} else if (args.format === 'markdown') {
|
|
126
|
+
content = docsJsonToMarkdown(res.data, { plainMarkdown: args.plainMarkdown });
|
|
122
127
|
} else {
|
|
123
128
|
// text
|
|
124
129
|
let textContent = '';
|
|
@@ -10,7 +10,7 @@ export function register(server) {
|
|
|
10
10
|
server.addTool({
|
|
11
11
|
name: 'appendMarkdown',
|
|
12
12
|
description: 'Best for adding new formatted content to the end of a document. ' +
|
|
13
|
-
'Supports headings, bold, italic, strikethrough, links,
|
|
13
|
+
'Supports headings, bold, italic, strikethrough, links, tables, bullet/numbered lists, and rich markdown HTML extensions for underline, color, highlight, font, alignment, and blockquotes. ' +
|
|
14
14
|
'Use this instead of appendText when you need formatting. ' +
|
|
15
15
|
'To edit existing content, use modifyText (single-location) or replaceDocumentWithMarkdown (section/full rewrite).',
|
|
16
16
|
parameters: DocumentIdParameter.extend({
|
|
@@ -10,7 +10,7 @@ export function register(server) {
|
|
|
10
10
|
server.addTool({
|
|
11
11
|
name: 'replaceDocumentWithMarkdown',
|
|
12
12
|
description: "Best for rewriting entire sections or full documents. Replaces the entire document body with content parsed from markdown. " +
|
|
13
|
-
"Supports headings, bold, italic, strikethrough, links,
|
|
13
|
+
"Supports headings, bold, italic, strikethrough, links, tables, bullet/numbered lists, and rich markdown HTML extensions for underline, color, highlight, font, alignment, and blockquotes. " +
|
|
14
14
|
"Use readDocument with format='markdown' first to get the current content, edit it, then call this tool to apply changes. " +
|
|
15
15
|
"For small single-location edits (one line or paragraph), use modifyText instead. " +
|
|
16
16
|
"To add content without rewriting, use appendMarkdown.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "google-tools-mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.12",
|
|
4
4
|
"description": "The easiest MCP server for Google Workspace — Drive, Docs, Sheets, Gmail, Calendar, and Forms. 153 tools with one-click browser auth. Read Word docs, PDFs, and spreadsheets straight from Drive.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -49,12 +49,13 @@
|
|
|
49
49
|
"ai-tools"
|
|
50
50
|
],
|
|
51
51
|
"scripts": {
|
|
52
|
+
"local:tool": "node scripts/call-local-tool.js",
|
|
52
53
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
53
54
|
"test:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js --ci --coverage"
|
|
54
55
|
},
|
|
55
56
|
"repository": {
|
|
56
57
|
"type": "git",
|
|
57
|
-
"url": "https://github.com/karthikcsq/google-tools-mcp.git"
|
|
58
|
+
"url": "git+https://github.com/karthikcsq/google-tools-mcp.git"
|
|
58
59
|
},
|
|
59
60
|
"homepage": "https://github.com/karthikcsq/google-tools-mcp#readme",
|
|
60
61
|
"bugs": {
|