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.
Files changed (60) hide show
  1. package/dist/googleDocsApiHelpers.js +220 -108
  2. package/dist/markdown-transformer/index.js +23 -3
  3. package/dist/markdown-transformer/markdownToDocs.js +25 -0
  4. package/dist/readTracker.js +84 -0
  5. package/dist/tools/docs/addTab.js +1 -0
  6. package/dist/tools/docs/appendToGoogleDoc.js +24 -4
  7. package/dist/tools/docs/comments/addComment.js +2 -1
  8. package/dist/tools/docs/comments/deleteComment.js +2 -1
  9. package/dist/tools/docs/comments/replyToComment.js +2 -1
  10. package/dist/tools/docs/comments/resolveComment.js +3 -2
  11. package/dist/tools/docs/deleteRange.js +5 -1
  12. package/dist/tools/docs/findAndReplace.js +5 -1
  13. package/dist/tools/docs/formatting/applyParagraphStyle.js +4 -3
  14. package/dist/tools/docs/insertImage.js +4 -2
  15. package/dist/tools/docs/insertPageBreak.js +2 -1
  16. package/dist/tools/docs/insertTable.js +2 -1
  17. package/dist/tools/docs/insertTableWithData.js +2 -1
  18. package/dist/tools/docs/modifyText.js +27 -11
  19. package/dist/tools/docs/modifyText.test.js +90 -0
  20. package/dist/tools/docs/readGoogleDoc.js +2 -0
  21. package/dist/tools/docs/renameTab.js +2 -1
  22. package/dist/tools/drive/deleteFile.js +2 -0
  23. package/dist/tools/drive/index.js +2 -0
  24. package/dist/tools/drive/moveFile.js +3 -2
  25. package/dist/tools/drive/uploadFile.js +111 -0
  26. package/dist/tools/extras/index.js +2 -0
  27. package/dist/tools/extras/readDriveFile.js +206 -0
  28. package/dist/tools/extras/readFile.js +2 -0
  29. package/dist/tools/forms/batchUpdateForm.js +1 -0
  30. package/dist/tools/forms/setPublishSettings.js +1 -0
  31. package/dist/tools/index.js +117 -11
  32. package/dist/tools/sheets/addConditionalFormatting.js +2 -1
  33. package/dist/tools/sheets/addSpreadsheetSheet.js +2 -1
  34. package/dist/tools/sheets/appendSpreadsheetRows.js +2 -1
  35. package/dist/tools/sheets/appendTableRows.js +1 -0
  36. package/dist/tools/sheets/autoResizeColumns.js +2 -1
  37. package/dist/tools/sheets/batchWrite.js +4 -1
  38. package/dist/tools/sheets/clearSpreadsheetRange.js +4 -1
  39. package/dist/tools/sheets/copyFormatting.js +2 -1
  40. package/dist/tools/sheets/createTable.js +1 -0
  41. package/dist/tools/sheets/deleteChart.js +2 -1
  42. package/dist/tools/sheets/deleteSheet.js +2 -1
  43. package/dist/tools/sheets/deleteTable.js +1 -0
  44. package/dist/tools/sheets/duplicateSheet.js +2 -1
  45. package/dist/tools/sheets/formatCells.js +2 -1
  46. package/dist/tools/sheets/freezeRowsAndColumns.js +2 -1
  47. package/dist/tools/sheets/getSpreadsheetInfo.js +2 -0
  48. package/dist/tools/sheets/groupRows.js +2 -1
  49. package/dist/tools/sheets/insertChart.js +2 -1
  50. package/dist/tools/sheets/readSpreadsheet.js +2 -0
  51. package/dist/tools/sheets/renameSheet.js +2 -1
  52. package/dist/tools/sheets/setColumnWidths.js +2 -1
  53. package/dist/tools/sheets/setDropdownValidation.js +3 -2
  54. package/dist/tools/sheets/ungroupAllRows.js +2 -1
  55. package/dist/tools/sheets/updateTableRange.js +1 -0
  56. package/dist/tools/sheets/writeSpreadsheet.js +4 -1
  57. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +27 -5
  58. package/dist/tools/utils/replaceDocumentWithMarkdown.js +28 -5
  59. package/dist/types.js +1 -2
  60. 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
- return `Chart created successfully${chartId ? ` (Chart ID: ${chartId})` : ''}.`;
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
- return `Successfully renamed sheet (ID: ${args.sheetId}) to "${args.newName}".`;
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
- return `Successfully set column widths: ${summary}.`;
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 `Successfully removed dropdown validation from range "${args.range}".`;
40
+ return `${sheetUrl}\nSuccessfully removed dropdown validation from range "${args.range}".`;
40
41
  }
41
- return `Successfully added dropdown validation to range "${args.range}" with ${args.values.length} options: ${args.values.join(', ')}.`;
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
- return `Successfully removed all row groups (${removed} level(s) cleared).`;
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
- return `Successfully wrote ${updatedCells} cells (${updatedRows} rows, ${updatedColumns} columns) to range ${args.range}.`;
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: '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
+ 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().min(1).describe('The markdown content to append.'),
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
- log.info(`Appending markdown to doc ${args.documentId} (${args.markdown.length} chars)${args.tabId ? ` in tab ${args.tabId}` : ''}`);
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, args.markdown, {
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
- return `Successfully appended ${args.markdown.length} characters of markdown.\n\n${debugSummary}`;
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. 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
+ 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().min(1).describe('The markdown content to apply to the document.'),
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
- log.info(`Replacing doc ${args.documentId} with markdown (${args.markdown.length} chars)${args.tabId ? ` in tab ${args.tabId}` : ''}`);
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, args.markdown, {
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
- return `Successfully replaced document content with ${args.markdown.length} characters of markdown.\n\n${debugSummary}`;
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
- .default(1)
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.0.16",
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
  }