google-tools-mcp 1.0.17 → 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/appendToGoogleDoc.js +22 -3
- package/dist/tools/docs/deleteRange.js +3 -0
- package/dist/tools/docs/findAndReplace.js +3 -0
- package/dist/tools/docs/formatting/applyParagraphStyle.js +2 -2
- package/dist/tools/docs/modifyText.js +25 -10
- package/dist/tools/docs/modifyText.test.js +90 -0
- package/dist/tools/docs/readGoogleDoc.js +2 -0
- package/dist/tools/drive/deleteFile.js +2 -0
- package/dist/tools/drive/index.js +2 -0
- 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/index.js +117 -11
- package/dist/tools/sheets/batchWrite.js +2 -0
- package/dist/tools/sheets/clearSpreadsheetRange.js +2 -0
- package/dist/tools/sheets/getSpreadsheetInfo.js +2 -0
- package/dist/tools/sheets/readSpreadsheet.js +2 -0
- package/dist/tools/sheets/writeSpreadsheet.js +2 -0
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +26 -5
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +27 -5
- package/dist/types.js +1 -2
- package/package.json +10 -2
package/dist/tools/index.js
CHANGED
|
@@ -6,21 +6,103 @@ import * as fs from 'fs/promises';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import * as os from 'os';
|
|
9
|
+
import { exec } from 'child_process';
|
|
10
|
+
import { promisify } from 'util';
|
|
9
11
|
import { getTokenPath, getConfigDir, SCOPES } from '../auth.js';
|
|
10
12
|
import { resetClients, withAuthRetry, getAuthClientIfReady } from '../clients.js';
|
|
11
13
|
import { logger } from '../logger.js';
|
|
12
14
|
import { google } from 'googleapis';
|
|
13
15
|
|
|
16
|
+
const execAsync = promisify(exec);
|
|
17
|
+
|
|
18
|
+
const REPO = 'karthikcsq/google-tools-mcp';
|
|
19
|
+
|
|
20
|
+
async function tryGhCli(title, body, label) {
|
|
21
|
+
// Probe for gh CLI
|
|
22
|
+
try {
|
|
23
|
+
await execAsync('gh --version');
|
|
24
|
+
} catch {
|
|
25
|
+
return { ok: false, reason: 'gh CLI not installed' };
|
|
26
|
+
}
|
|
27
|
+
// Probe for auth
|
|
28
|
+
try {
|
|
29
|
+
await execAsync('gh auth status');
|
|
30
|
+
} catch {
|
|
31
|
+
return { ok: false, reason: 'gh CLI not authenticated (run: gh auth login)' };
|
|
32
|
+
}
|
|
33
|
+
// Write body to a temp file to avoid shell-escaping issues with newlines/quotes.
|
|
34
|
+
const tmpFile = path.join(os.tmpdir(), `gtm-feedback-${Date.now()}-${Math.random().toString(36).slice(2)}.md`);
|
|
35
|
+
try {
|
|
36
|
+
await fs.writeFile(tmpFile, body, 'utf8');
|
|
37
|
+
const { stdout } = await execAsync(
|
|
38
|
+
`gh issue create --repo ${REPO} --title ${JSON.stringify(title)} --label ${JSON.stringify(label)} --body-file ${JSON.stringify(tmpFile)}`,
|
|
39
|
+
{ maxBuffer: 10 * 1024 * 1024 }
|
|
40
|
+
);
|
|
41
|
+
const issueUrl = stdout.trim().split('\n').pop();
|
|
42
|
+
return { ok: true, issueUrl };
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return { ok: false, reason: `gh CLI failed: ${err.stderr || err.message || err}` };
|
|
45
|
+
} finally {
|
|
46
|
+
try { await fs.unlink(tmpFile); } catch {}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function openBrowser(url) {
|
|
51
|
+
const platform = process.platform;
|
|
52
|
+
let cmd;
|
|
53
|
+
if (platform === 'win32') {
|
|
54
|
+
cmd = `start "" "${url}"`;
|
|
55
|
+
} else if (platform === 'darwin') {
|
|
56
|
+
cmd = `open "${url}"`;
|
|
57
|
+
} else {
|
|
58
|
+
cmd = `xdg-open "${url}"`;
|
|
59
|
+
}
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
exec(cmd, (err) => resolve(!err));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
14
65
|
// ---------------------------------------------------------------------------
|
|
15
|
-
// Wrap server.addTool so every tool's execute() auto-retries on invalid_grant
|
|
66
|
+
// Wrap server.addTool so every tool's execute() auto-retries on invalid_grant
|
|
67
|
+
// and appends a troubleshoot/feedback hint to errors.
|
|
16
68
|
// ---------------------------------------------------------------------------
|
|
69
|
+
const ERROR_HINT =
|
|
70
|
+
'\n\nIf this error is unexpected or unclear, you can:\n' +
|
|
71
|
+
' • Call the `troubleshoot` tool to run a health check (auth, API connectivity, recent logs).\n' +
|
|
72
|
+
' • Call the `feedback` tool to file a bug report with diagnostics auto-attached.';
|
|
73
|
+
|
|
74
|
+
// Tools that should NOT have the hint appended (would be circular/noisy).
|
|
75
|
+
const HINT_EXCLUDED_TOOLS = new Set(['troubleshoot', 'feedback', 'help', 'logout']);
|
|
76
|
+
|
|
77
|
+
function appendHintToError(error, toolName) {
|
|
78
|
+
if (HINT_EXCLUDED_TOOLS.has(toolName)) return error;
|
|
79
|
+
if (!error) return error;
|
|
80
|
+
// Avoid double-appending if something else (or a retry) already added it.
|
|
81
|
+
const existingMsg = error.message || '';
|
|
82
|
+
if (existingMsg.includes('`troubleshoot` tool')) return error;
|
|
83
|
+
try {
|
|
84
|
+
error.message = existingMsg + ERROR_HINT;
|
|
85
|
+
} catch {
|
|
86
|
+
// Some error types have non-writable message; fall back to a new error.
|
|
87
|
+
const wrapped = new Error(existingMsg + ERROR_HINT);
|
|
88
|
+
wrapped.cause = error;
|
|
89
|
+
return wrapped;
|
|
90
|
+
}
|
|
91
|
+
return error;
|
|
92
|
+
}
|
|
93
|
+
|
|
17
94
|
function wrapServerWithAuthRetry(server) {
|
|
18
95
|
const originalAddTool = server.addTool.bind(server);
|
|
19
96
|
server.addTool = function (toolDef) {
|
|
20
97
|
const originalExecute = toolDef.execute;
|
|
98
|
+
const toolName = toolDef.name;
|
|
21
99
|
if (originalExecute) {
|
|
22
|
-
toolDef.execute = function (...args) {
|
|
23
|
-
|
|
100
|
+
toolDef.execute = async function (...args) {
|
|
101
|
+
try {
|
|
102
|
+
return await withAuthRetry(() => originalExecute.apply(this, args));
|
|
103
|
+
} catch (err) {
|
|
104
|
+
throw appendHintToError(err, toolName);
|
|
105
|
+
}
|
|
24
106
|
};
|
|
25
107
|
}
|
|
26
108
|
return originalAddTool(toolDef);
|
|
@@ -109,15 +191,22 @@ export async function registerAllTools(server) {
|
|
|
109
191
|
server.addTool({
|
|
110
192
|
name: 'help',
|
|
111
193
|
description:
|
|
112
|
-
'Show documentation for google-tools-mcp: setup instructions, available tool categories, environment variables, and troubleshooting.
|
|
194
|
+
'Show documentation for google-tools-mcp: setup instructions, available tool categories, environment variables, and troubleshooting. ' +
|
|
195
|
+
'Call this when you need guidance on how to use the Google Workspace tools. ' +
|
|
196
|
+
'Also available: `troubleshoot` (run health check when tools fail) and `feedback` (submit bug reports/feature requests with diagnostics auto-attached).',
|
|
113
197
|
parameters: z.object({}),
|
|
114
198
|
execute: async () => {
|
|
115
199
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
116
200
|
const readmePath = path.resolve(__dirname, '..', '..', 'README.md');
|
|
201
|
+
const diagnosticsSection = '\n\n## Diagnostics & Feedback\n\n' +
|
|
202
|
+
'- **`troubleshoot`** — Run a health check when tools fail (checks auth, API connectivity, config, recent logs).\n' +
|
|
203
|
+
'- **`feedback`** — Submit a bug report or feature request with diagnostics auto-attached (files via GitHub CLI or browser).\n' +
|
|
204
|
+
'- **`help`** — Show this documentation.\n';
|
|
117
205
|
try {
|
|
118
|
-
|
|
206
|
+
const readme = await fs.readFile(readmePath, 'utf-8');
|
|
207
|
+
return readme + diagnosticsSection;
|
|
119
208
|
} catch {
|
|
120
|
-
return 'README not found. Visit https://www.npmjs.com/package/google-tools-mcp for documentation.';
|
|
209
|
+
return 'README not found. Visit https://www.npmjs.com/package/google-tools-mcp for documentation.' + diagnosticsSection;
|
|
121
210
|
}
|
|
122
211
|
},
|
|
123
212
|
});
|
|
@@ -275,7 +364,7 @@ export async function registerAllTools(server) {
|
|
|
275
364
|
server.addTool({
|
|
276
365
|
name: 'feedback',
|
|
277
366
|
description:
|
|
278
|
-
'Submit feedback or a bug report for google-tools-mcp. Automatically collects diagnostic info
|
|
367
|
+
'Submit feedback or a bug report for google-tools-mcp. Automatically collects diagnostic info, then files the issue via the GitHub CLI (`gh`) if available, or falls back to opening a pre-filled GitHub issue URL in the user\'s browser.',
|
|
279
368
|
parameters: z.object({
|
|
280
369
|
type: z.enum(['bug', 'feature']).describe('Type of feedback'),
|
|
281
370
|
title: z.string().describe('Short summary'),
|
|
@@ -334,21 +423,38 @@ export async function registerAllTools(server) {
|
|
|
334
423
|
`</details>`,
|
|
335
424
|
].join('\n');
|
|
336
425
|
|
|
337
|
-
// Build GitHub issue URL
|
|
338
426
|
const label = args.type === 'bug' ? 'bug' : 'enhancement';
|
|
427
|
+
|
|
428
|
+
// Try gh CLI first
|
|
429
|
+
const ghResult = await tryGhCli(args.title, body, label);
|
|
430
|
+
if (ghResult.ok) {
|
|
431
|
+
return JSON.stringify({
|
|
432
|
+
method: 'gh-cli',
|
|
433
|
+
issueUrl: ghResult.issueUrl,
|
|
434
|
+
note: 'Issue filed successfully via GitHub CLI.',
|
|
435
|
+
}, null, 2);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Fallback: open pre-filled GitHub issue URL in the user's browser
|
|
339
439
|
const params = new URLSearchParams({
|
|
340
440
|
title: args.title,
|
|
341
441
|
body,
|
|
342
442
|
labels: label,
|
|
343
443
|
});
|
|
344
|
-
const url = `https://github.com/
|
|
444
|
+
const url = `https://github.com/${REPO}/issues/new?${params.toString()}`;
|
|
445
|
+
const opened = await openBrowser(url);
|
|
345
446
|
|
|
346
447
|
return JSON.stringify({
|
|
448
|
+
method: 'browser-fallback',
|
|
449
|
+
ghCliUnavailableReason: ghResult.reason,
|
|
347
450
|
url,
|
|
451
|
+
browserOpened: opened,
|
|
348
452
|
markdown: body,
|
|
349
453
|
note: url.length > 8000
|
|
350
|
-
? 'The URL may be too long for some browsers. Use the markdown body
|
|
351
|
-
:
|
|
454
|
+
? 'The URL may be too long for some browsers. Use the markdown body to create the issue manually.'
|
|
455
|
+
: opened
|
|
456
|
+
? 'Opened the pre-filled GitHub issue in your browser. Click "Submit new issue" to file it.'
|
|
457
|
+
: 'Could not auto-open browser. Please open the URL manually.',
|
|
352
458
|
}, null, 2);
|
|
353
459
|
},
|
|
354
460
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { UserError } from 'fastmcp';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
import { guardMutation, trackMutation } from '../../readTracker.js';
|
|
4
5
|
export function register(server) {
|
|
5
6
|
server.addTool({
|
|
6
7
|
name: 'batchWrite',
|
|
@@ -25,6 +26,7 @@ export function register(server) {
|
|
|
25
26
|
.describe('How input data should be interpreted. RAW: values are stored as-is. USER_ENTERED: values are parsed as if typed by a user.'),
|
|
26
27
|
}),
|
|
27
28
|
execute: async (args, { log }) => {
|
|
29
|
+
await guardMutation(args.spreadsheetId);
|
|
28
30
|
const sheets = await getSheetsClient();
|
|
29
31
|
const rangeNames = args.data.map((d) => d.range).join(', ');
|
|
30
32
|
log.info(`Batch writing to ${args.data.length} range(s) in spreadsheet ${args.spreadsheetId}: ${rangeNames}`);
|
|
@@ -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: 'clearRange',
|
|
@@ -13,6 +14,7 @@ export function register(server) {
|
|
|
13
14
|
range: z.string().describe('A1 notation range to clear (e.g., "A1:B10" or "Sheet1!A1:B10").'),
|
|
14
15
|
}),
|
|
15
16
|
execute: async (args, { log }) => {
|
|
17
|
+
await guardMutation(args.spreadsheetId);
|
|
16
18
|
const sheets = await getSheetsClient();
|
|
17
19
|
log.info(`Clearing range ${args.range} in spreadsheet ${args.spreadsheetId}`);
|
|
18
20
|
try {
|
|
@@ -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: 'getSpreadsheetInfo',
|
|
@@ -16,6 +17,7 @@ export function register(server) {
|
|
|
16
17
|
log.info(`Getting info for spreadsheet: ${args.spreadsheetId}`);
|
|
17
18
|
try {
|
|
18
19
|
const metadata = await SheetsHelpers.getSpreadsheetMetadata(sheets, args.spreadsheetId);
|
|
20
|
+
trackRead(args.spreadsheetId);
|
|
19
21
|
const sheetList = metadata.sheets || [];
|
|
20
22
|
return JSON.stringify({
|
|
21
23
|
title: metadata.properties?.title || 'Untitled',
|
|
@@ -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
|
}
|
|
@@ -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 {
|
|
@@ -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,7 +92,7 @@ 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,
|
|
@@ -80,7 +100,8 @@ export function register(server) {
|
|
|
80
100
|
const debugSummary = formatInsertResult(result);
|
|
81
101
|
log.info(debugSummary);
|
|
82
102
|
const docUrl = `https://docs.google.com/document/d/${args.documentId}/edit`;
|
|
83
|
-
|
|
103
|
+
trackMutation(args.documentId);
|
|
104
|
+
return `${docUrl}\nSuccessfully appended ${markdown.length} characters of markdown.\n\n${debugSummary}`;
|
|
84
105
|
}
|
|
85
106
|
catch (error) {
|
|
86
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,15 +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);
|
|
164
|
+
trackMutation(args.documentId);
|
|
143
165
|
const docUrl = `https://docs.google.com/document/d/${args.documentId}/edit`;
|
|
144
|
-
return `${docUrl}\nSuccessfully replaced document content with ${
|
|
166
|
+
return `${docUrl}\nSuccessfully replaced document content with ${markdown.length} characters of markdown.\n\n${debugSummary}`;
|
|
145
167
|
}
|
|
146
168
|
catch (error) {
|
|
147
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
|
}
|