google-tools-mcp 1.0.16 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/googleDocsApiHelpers.js +220 -108
- package/dist/markdown-transformer/index.js +23 -3
- package/dist/markdown-transformer/markdownToDocs.js +25 -0
- package/dist/readTracker.js +84 -0
- package/dist/tools/docs/addTab.js +1 -0
- package/dist/tools/docs/appendToGoogleDoc.js +24 -4
- package/dist/tools/docs/comments/addComment.js +2 -1
- package/dist/tools/docs/comments/deleteComment.js +2 -1
- package/dist/tools/docs/comments/replyToComment.js +2 -1
- package/dist/tools/docs/comments/resolveComment.js +3 -2
- package/dist/tools/docs/deleteRange.js +5 -1
- package/dist/tools/docs/findAndReplace.js +5 -1
- package/dist/tools/docs/formatting/applyParagraphStyle.js +4 -3
- package/dist/tools/docs/insertImage.js +4 -2
- package/dist/tools/docs/insertPageBreak.js +2 -1
- package/dist/tools/docs/insertTable.js +2 -1
- package/dist/tools/docs/insertTableWithData.js +2 -1
- package/dist/tools/docs/modifyText.js +27 -11
- package/dist/tools/docs/modifyText.test.js +90 -0
- package/dist/tools/docs/readGoogleDoc.js +2 -0
- package/dist/tools/docs/renameTab.js +2 -1
- package/dist/tools/drive/deleteFile.js +2 -0
- package/dist/tools/drive/index.js +2 -0
- package/dist/tools/drive/moveFile.js +3 -2
- package/dist/tools/drive/uploadFile.js +111 -0
- package/dist/tools/extras/index.js +2 -0
- package/dist/tools/extras/readDriveFile.js +206 -0
- package/dist/tools/extras/readFile.js +2 -0
- package/dist/tools/forms/batchUpdateForm.js +1 -0
- package/dist/tools/forms/setPublishSettings.js +1 -0
- package/dist/tools/index.js +117 -11
- package/dist/tools/sheets/addConditionalFormatting.js +2 -1
- package/dist/tools/sheets/addSpreadsheetSheet.js +2 -1
- package/dist/tools/sheets/appendSpreadsheetRows.js +2 -1
- package/dist/tools/sheets/appendTableRows.js +1 -0
- package/dist/tools/sheets/autoResizeColumns.js +2 -1
- package/dist/tools/sheets/batchWrite.js +4 -1
- package/dist/tools/sheets/clearSpreadsheetRange.js +4 -1
- package/dist/tools/sheets/copyFormatting.js +2 -1
- package/dist/tools/sheets/createTable.js +1 -0
- package/dist/tools/sheets/deleteChart.js +2 -1
- package/dist/tools/sheets/deleteSheet.js +2 -1
- package/dist/tools/sheets/deleteTable.js +1 -0
- package/dist/tools/sheets/duplicateSheet.js +2 -1
- package/dist/tools/sheets/formatCells.js +2 -1
- package/dist/tools/sheets/freezeRowsAndColumns.js +2 -1
- package/dist/tools/sheets/getSpreadsheetInfo.js +2 -0
- package/dist/tools/sheets/groupRows.js +2 -1
- package/dist/tools/sheets/insertChart.js +2 -1
- package/dist/tools/sheets/readSpreadsheet.js +2 -0
- package/dist/tools/sheets/renameSheet.js +2 -1
- package/dist/tools/sheets/setColumnWidths.js +2 -1
- package/dist/tools/sheets/setDropdownValidation.js +3 -2
- package/dist/tools/sheets/ungroupAllRows.js +2 -1
- package/dist/tools/sheets/updateTableRange.js +1 -0
- package/dist/tools/sheets/writeSpreadsheet.js +4 -1
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +27 -5
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +28 -5
- package/dist/types.js +1 -2
- package/package.json +10 -2
|
@@ -212,7 +212,8 @@ export function register(server) {
|
|
|
212
212
|
},
|
|
213
213
|
});
|
|
214
214
|
const chartId = response.data.replies?.[0]?.addChart?.chart?.chartId;
|
|
215
|
-
|
|
215
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
216
|
+
return `${sheetUrl}\nChart created successfully${chartId ? ` (Chart ID: ${chartId})` : ''}.`;
|
|
216
217
|
}
|
|
217
218
|
catch (error) {
|
|
218
219
|
log.error(`Error inserting chart: ${error.message || error}`);
|
|
@@ -2,6 +2,7 @@ import { UserError } from 'fastmcp';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getSheetsClient } from '../../clients.js';
|
|
4
4
|
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
import { trackRead } from '../../readTracker.js';
|
|
5
6
|
export function register(server) {
|
|
6
7
|
server.addTool({
|
|
7
8
|
name: 'readSpreadsheet',
|
|
@@ -22,6 +23,7 @@ export function register(server) {
|
|
|
22
23
|
log.info(`Reading spreadsheet ${args.spreadsheetId}, range: ${args.range}`);
|
|
23
24
|
try {
|
|
24
25
|
const response = await SheetsHelpers.readRange(sheets, args.spreadsheetId, args.range, args.valueRenderOption);
|
|
26
|
+
trackRead(args.spreadsheetId);
|
|
25
27
|
const values = response.values || [];
|
|
26
28
|
return JSON.stringify({ range: args.range, values }, null, 2);
|
|
27
29
|
}
|
|
@@ -35,7 +35,8 @@ export function register(server) {
|
|
|
35
35
|
],
|
|
36
36
|
},
|
|
37
37
|
});
|
|
38
|
-
|
|
38
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
39
|
+
return `${sheetUrl}\nSuccessfully renamed sheet (ID: ${args.sheetId}) to "${args.newName}".`;
|
|
39
40
|
}
|
|
40
41
|
catch (error) {
|
|
41
42
|
log.error(`Error renaming sheet in spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
@@ -30,7 +30,8 @@ export function register(server) {
|
|
|
30
30
|
try {
|
|
31
31
|
await SheetsHelpers.setColumnWidths(sheets, args.spreadsheetId, args.sheetName, args.columnWidths);
|
|
32
32
|
const summary = args.columnWidths.map((cw) => `${cw.column}=${cw.width}px`).join(', ');
|
|
33
|
-
|
|
33
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
34
|
+
return `${sheetUrl}\nSuccessfully set column widths: ${summary}.`;
|
|
34
35
|
}
|
|
35
36
|
catch (error) {
|
|
36
37
|
log.error(`Error setting column widths: ${error.message || error}`);
|
|
@@ -35,10 +35,11 @@ export function register(server) {
|
|
|
35
35
|
: `Setting dropdown validation on "${args.range}" with ${args.values.length} options in spreadsheet ${args.spreadsheetId}`);
|
|
36
36
|
try {
|
|
37
37
|
await SheetsHelpers.setDropdownValidation(sheets, args.spreadsheetId, args.range, args.values, args.strict, args.inputMessage);
|
|
38
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
38
39
|
if (isClearing) {
|
|
39
|
-
return
|
|
40
|
+
return `${sheetUrl}\nSuccessfully removed dropdown validation from range "${args.range}".`;
|
|
40
41
|
}
|
|
41
|
-
return
|
|
42
|
+
return `${sheetUrl}\nSuccessfully added dropdown validation to range "${args.range}" with ${args.values.length} options: ${args.values.join(', ')}.`;
|
|
42
43
|
}
|
|
43
44
|
catch (error) {
|
|
44
45
|
log.error(`Error setting dropdown validation: ${error.message || error}`);
|
|
@@ -53,7 +53,8 @@ export function register(server) {
|
|
|
53
53
|
break;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
57
|
+
return `${sheetUrl}\nSuccessfully removed all row groups (${removed} level(s) cleared).`;
|
|
57
58
|
}
|
|
58
59
|
catch (error) {
|
|
59
60
|
log.error(`Error removing row groups: ${error.message || error}`);
|
|
@@ -30,6 +30,7 @@ export function register(server) {
|
|
|
30
30
|
// Update the table range
|
|
31
31
|
const updatedTable = await SheetsHelpers.updateTableRangeHelper(sheets, args.spreadsheetId, table.tableId || '', newRange);
|
|
32
32
|
return JSON.stringify({
|
|
33
|
+
url: `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`,
|
|
33
34
|
tableId: updatedTable.tableId,
|
|
34
35
|
name: updatedTable.name,
|
|
35
36
|
oldRange: table.range
|
|
@@ -2,6 +2,7 @@ import { UserError } from 'fastmcp';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getSheetsClient } from '../../clients.js';
|
|
4
4
|
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
import { guardMutation, trackMutation } from '../../readTracker.js';
|
|
5
6
|
export function register(server) {
|
|
6
7
|
server.addTool({
|
|
7
8
|
name: 'writeSpreadsheet',
|
|
@@ -23,6 +24,7 @@ export function register(server) {
|
|
|
23
24
|
.describe('How input data should be interpreted. RAW: values are stored as-is. USER_ENTERED: values are parsed as if typed by a user.'),
|
|
24
25
|
}),
|
|
25
26
|
execute: async (args, { log }) => {
|
|
27
|
+
await guardMutation(args.spreadsheetId);
|
|
26
28
|
const sheets = await getSheetsClient();
|
|
27
29
|
log.info(`Writing to spreadsheet ${args.spreadsheetId}, range: ${args.range}`);
|
|
28
30
|
try {
|
|
@@ -30,7 +32,8 @@ export function register(server) {
|
|
|
30
32
|
const updatedCells = response.updatedCells || 0;
|
|
31
33
|
const updatedRows = response.updatedRows || 0;
|
|
32
34
|
const updatedColumns = response.updatedColumns || 0;
|
|
33
|
-
|
|
35
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
36
|
+
return `${sheetUrl}\nSuccessfully wrote ${updatedCells} cells (${updatedRows} rows, ${updatedColumns} columns) to range ${args.range}.`;
|
|
34
37
|
}
|
|
35
38
|
catch (error) {
|
|
36
39
|
log.error(`Error writing to spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
1
2
|
import { UserError } from 'fastmcp';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { getDocsClient } from '../../clients.js';
|
|
4
5
|
import { DocumentIdParameter, MarkdownConversionError } from '../../types.js';
|
|
5
6
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
7
|
import { insertMarkdown, formatInsertResult } from '../../markdown-transformer/index.js';
|
|
8
|
+
import { guardMutation, trackMutation } from '../../readTracker.js';
|
|
7
9
|
export function register(server) {
|
|
8
10
|
server.addTool({
|
|
9
11
|
name: 'appendMarkdown',
|
|
10
|
-
description: '
|
|
12
|
+
description: 'Best for adding new formatted content to the end of a document. ' +
|
|
13
|
+
'Supports headings, bold, italic, strikethrough, links, and bullet/numbered lists. ' +
|
|
14
|
+
'Use this instead of appendText when you need formatting. ' +
|
|
15
|
+
'To edit existing content, use modifyText (single-location) or replaceDocumentWithMarkdown (section/full rewrite).',
|
|
11
16
|
parameters: DocumentIdParameter.extend({
|
|
12
|
-
markdown: z.string().
|
|
17
|
+
markdown: z.string().optional().describe('The markdown content to append. For content longer than ~2000 characters, prefer writing to a local file first and passing filePath instead.'),
|
|
18
|
+
filePath: z.string().optional().describe('Path to a local markdown file to use as content. Takes precedence over the markdown parameter.'),
|
|
13
19
|
addNewlineIfNeeded: z
|
|
14
20
|
.boolean()
|
|
15
21
|
.optional()
|
|
@@ -26,8 +32,22 @@ export function register(server) {
|
|
|
26
32
|
.describe('If true, the first H1 heading (# ...) in the markdown is styled as a Google Docs TITLE instead of Heading 1. Useful when the markdown represents a full document whose first line is the document title.'),
|
|
27
33
|
}),
|
|
28
34
|
execute: async (args, { log }) => {
|
|
35
|
+
await guardMutation(args.documentId);
|
|
29
36
|
const docs = await getDocsClient();
|
|
30
|
-
|
|
37
|
+
// Resolve markdown content from filePath or inline parameter
|
|
38
|
+
let markdown = args.markdown;
|
|
39
|
+
if (args.filePath) {
|
|
40
|
+
try {
|
|
41
|
+
markdown = await fs.readFile(args.filePath, 'utf-8');
|
|
42
|
+
log.info(`Read ${markdown.length} chars from file: ${args.filePath}`);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new UserError(`Failed to read file at "${args.filePath}": ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!markdown || markdown.length === 0) {
|
|
48
|
+
throw new UserError('Either markdown or filePath must be provided with non-empty content.');
|
|
49
|
+
}
|
|
50
|
+
log.info(`Appending markdown to doc ${args.documentId} (${markdown.length} chars)${args.tabId ? ` in tab ${args.tabId}` : ''}`);
|
|
31
51
|
try {
|
|
32
52
|
// 1. Get document end index
|
|
33
53
|
const doc = await docs.documents.get({
|
|
@@ -72,14 +92,16 @@ export function register(server) {
|
|
|
72
92
|
log.info(`Added spacing, new start index: ${startIndex}`);
|
|
73
93
|
}
|
|
74
94
|
// 3. Convert and append markdown
|
|
75
|
-
const result = await insertMarkdown(docs, args.documentId,
|
|
95
|
+
const result = await insertMarkdown(docs, args.documentId, markdown, {
|
|
76
96
|
startIndex,
|
|
77
97
|
tabId: args.tabId,
|
|
78
98
|
firstHeadingAsTitle: args.firstHeadingAsTitle,
|
|
79
99
|
});
|
|
80
100
|
const debugSummary = formatInsertResult(result);
|
|
81
101
|
log.info(debugSummary);
|
|
82
|
-
|
|
102
|
+
const docUrl = `https://docs.google.com/document/d/${args.documentId}/edit`;
|
|
103
|
+
trackMutation(args.documentId);
|
|
104
|
+
return `${docUrl}\nSuccessfully appended ${markdown.length} characters of markdown.\n\n${debugSummary}`;
|
|
83
105
|
}
|
|
84
106
|
catch (error) {
|
|
85
107
|
log.error(`Error appending markdown: ${error.message}`);
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
1
2
|
import { UserError } from 'fastmcp';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { getDocsClient } from '../../clients.js';
|
|
4
5
|
import { DocumentIdParameter, MarkdownConversionError } from '../../types.js';
|
|
5
6
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
7
|
import { insertMarkdown, formatInsertResult } from '../../markdown-transformer/index.js';
|
|
8
|
+
import { guardMutation, trackMutation } from '../../readTracker.js';
|
|
7
9
|
export function register(server) {
|
|
8
10
|
server.addTool({
|
|
9
11
|
name: 'replaceDocumentWithMarkdown',
|
|
10
|
-
description: "Replaces the entire document body with content parsed from markdown.
|
|
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, and bullet/numbered lists. " +
|
|
14
|
+
"Use readDocument with format='markdown' first to get the current content, edit it, then call this tool to apply changes. " +
|
|
15
|
+
"For small single-location edits (one line or paragraph), use modifyText instead. " +
|
|
16
|
+
"To add content without rewriting, use appendMarkdown.",
|
|
11
17
|
parameters: DocumentIdParameter.extend({
|
|
12
|
-
markdown: z.string().
|
|
18
|
+
markdown: z.string().optional().describe('The markdown content to apply to the document. For content longer than ~2000 characters, prefer writing to a local file first and passing filePath instead.'),
|
|
19
|
+
filePath: z.string().optional().describe('Path to a local markdown file to use as content. Takes precedence over the markdown parameter. Use this for large documents to avoid truncation.'),
|
|
13
20
|
preserveTitle: z
|
|
14
21
|
.boolean()
|
|
15
22
|
.optional()
|
|
@@ -26,8 +33,22 @@ export function register(server) {
|
|
|
26
33
|
.describe('If true (default), the first H1 heading (# ...) in the markdown is styled as a Google Docs TITLE instead of Heading 1. Useful when the markdown represents a full document whose first line is the document title. Set to false if the first H1 should remain a Heading 1.'),
|
|
27
34
|
}),
|
|
28
35
|
execute: async (args, { log }) => {
|
|
36
|
+
await guardMutation(args.documentId);
|
|
29
37
|
const docs = await getDocsClient();
|
|
30
|
-
|
|
38
|
+
// Resolve markdown content from filePath or inline parameter
|
|
39
|
+
let markdown = args.markdown;
|
|
40
|
+
if (args.filePath) {
|
|
41
|
+
try {
|
|
42
|
+
markdown = await fs.readFile(args.filePath, 'utf-8');
|
|
43
|
+
log.info(`Read ${markdown.length} chars from file: ${args.filePath}`);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
throw new UserError(`Failed to read file at "${args.filePath}": ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!markdown || markdown.length === 0) {
|
|
49
|
+
throw new UserError('Either markdown or filePath must be provided with non-empty content.');
|
|
50
|
+
}
|
|
51
|
+
log.info(`Replacing doc ${args.documentId} with markdown (${markdown.length} chars)${args.tabId ? ` in tab ${args.tabId}` : ''}`);
|
|
31
52
|
try {
|
|
32
53
|
// 1. Get document structure
|
|
33
54
|
const doc = await docs.documents.get({
|
|
@@ -133,14 +154,16 @@ export function register(server) {
|
|
|
133
154
|
}
|
|
134
155
|
// 5. Convert markdown and insert (indices calculated for empty document)
|
|
135
156
|
log.info(`Inserting markdown starting at index ${startIndex} (after delete, document should be empty)`);
|
|
136
|
-
const result = await insertMarkdown(docs, args.documentId,
|
|
157
|
+
const result = await insertMarkdown(docs, args.documentId, markdown, {
|
|
137
158
|
startIndex,
|
|
138
159
|
tabId: args.tabId,
|
|
139
160
|
firstHeadingAsTitle: args.firstHeadingAsTitle,
|
|
140
161
|
});
|
|
141
162
|
const debugSummary = formatInsertResult(result);
|
|
142
163
|
log.info(debugSummary);
|
|
143
|
-
|
|
164
|
+
trackMutation(args.documentId);
|
|
165
|
+
const docUrl = `https://docs.google.com/document/d/${args.documentId}/edit`;
|
|
166
|
+
return `${docUrl}\nSuccessfully replaced document content with ${markdown.length} characters of markdown.\n\n${debugSummary}`;
|
|
144
167
|
}
|
|
145
168
|
catch (error) {
|
|
146
169
|
log.error(`Error replacing document with markdown: ${error.message}`);
|
package/dist/types.js
CHANGED
|
@@ -66,8 +66,7 @@ export const TextFindParameter = z.object({
|
|
|
66
66
|
.int()
|
|
67
67
|
.min(1)
|
|
68
68
|
.optional()
|
|
69
|
-
.
|
|
70
|
-
.describe('Which instance of the text to target (1st, 2nd, etc.). Defaults to 1.'),
|
|
69
|
+
.describe('Which instance of the text to target (1st, 2nd, etc.). Required when multiple matches exist — omit to auto-select if only one match.'),
|
|
71
70
|
});
|
|
72
71
|
// --- Style Parameter Schemas ---
|
|
73
72
|
export const TextStyleParameters = z
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "google-tools-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Combined Google Workspace MCP server (Drive, Docs, Sheets, Gmail) with lazy-loaded tool categories",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,14 +17,22 @@
|
|
|
17
17
|
"sheets",
|
|
18
18
|
"docs"
|
|
19
19
|
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
22
|
+
"test:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js --ci --coverage"
|
|
23
|
+
},
|
|
20
24
|
"license": "MIT",
|
|
21
25
|
"dependencies": {
|
|
22
26
|
"fastmcp": "^3.24.0",
|
|
23
27
|
"google-auth-library": "^10.5.0",
|
|
24
28
|
"googleapis": "^171.4.0",
|
|
25
|
-
"markdown-it": "^14.1.0",
|
|
26
29
|
"mammoth": "^1.9.0",
|
|
30
|
+
"markdown-it": "^14.1.0",
|
|
27
31
|
"pdf-parse": "^1.1.1",
|
|
28
32
|
"zod": "^3.24.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@jest/globals": "^30.3.0",
|
|
36
|
+
"jest": "^30.3.0"
|
|
29
37
|
}
|
|
30
38
|
}
|