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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriveClient, getDocsClient, getSheetsClient } from '../../clients.js';
|
|
4
|
+
import { docsJsonToMarkdown } from '../../markdown-transformer/index.js';
|
|
5
|
+
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
+
import { extractText, downloadBuffer } from './readFile.js';
|
|
7
|
+
import { trackRead } from '../../readTracker.js';
|
|
8
|
+
|
|
9
|
+
// Google-native MIME types
|
|
10
|
+
const GOOGLE_DOC = 'application/vnd.google-apps.document';
|
|
11
|
+
const GOOGLE_SHEET = 'application/vnd.google-apps.spreadsheet';
|
|
12
|
+
const GOOGLE_SLIDES = 'application/vnd.google-apps.presentation';
|
|
13
|
+
|
|
14
|
+
// Capabilities by file type category
|
|
15
|
+
const CAPABILITIES = {
|
|
16
|
+
googleDoc: {
|
|
17
|
+
fileType: 'Google Doc',
|
|
18
|
+
capabilities: ['read', 'readDocument', 'modifyText', 'replaceDocumentWithMarkdown', 'appendMarkdown', 'appendText', 'findAndReplace', 'deleteRange'],
|
|
19
|
+
},
|
|
20
|
+
googleSheet: {
|
|
21
|
+
fileType: 'Google Sheet',
|
|
22
|
+
capabilities: ['read', 'readSpreadsheet', 'writeSpreadsheet', 'appendRows', 'formatCells'],
|
|
23
|
+
},
|
|
24
|
+
docx: {
|
|
25
|
+
fileType: 'Word Document (.docx)',
|
|
26
|
+
capabilities: ['read'],
|
|
27
|
+
note: 'This is a Word document. Editing tools (modifyText, appendMarkdown, etc.) are not available unless converted to a Google Doc via copyFile or Drive UI.',
|
|
28
|
+
},
|
|
29
|
+
pdf: {
|
|
30
|
+
fileType: 'PDF',
|
|
31
|
+
capabilities: ['read'],
|
|
32
|
+
note: 'This is a PDF file. Editing tools are not available. Convert to a Google Doc first if editing is needed.',
|
|
33
|
+
},
|
|
34
|
+
other: {
|
|
35
|
+
fileType: 'Unknown',
|
|
36
|
+
capabilities: ['download'],
|
|
37
|
+
note: 'This file type cannot be read as text. Use downloadFile to download it.',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function register(server) {
|
|
42
|
+
server.addTool({
|
|
43
|
+
name: 'readDriveFile',
|
|
44
|
+
description:
|
|
45
|
+
'Unified read tool: accepts any Google Drive file ID, auto-detects the file type (Google Doc, Sheet, Word .docx, PDF), ' +
|
|
46
|
+
'and routes to the appropriate reader. Returns content along with metadata about available capabilities. ' +
|
|
47
|
+
'Use this when you have a file ID but don\'t know the file type, or when readDocument fails with a 400 error.',
|
|
48
|
+
parameters: z.object({
|
|
49
|
+
fileId: z
|
|
50
|
+
.string()
|
|
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
|
+
range: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("A1 notation range for Google Sheets (e.g., 'Sheet1!A1:C10'). Required for spreadsheets."),
|
|
61
|
+
}),
|
|
62
|
+
execute: async (args, { log }) => {
|
|
63
|
+
const drive = await getDriveClient();
|
|
64
|
+
|
|
65
|
+
// 1. Get file metadata to determine type
|
|
66
|
+
let fileMeta;
|
|
67
|
+
try {
|
|
68
|
+
const res = await drive.files.get({
|
|
69
|
+
fileId: args.fileId,
|
|
70
|
+
fields: 'id,name,mimeType,modifiedTime,size',
|
|
71
|
+
supportsAllDrives: true,
|
|
72
|
+
});
|
|
73
|
+
fileMeta = res.data;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error.code === 404) throw new UserError(`File not found: ${args.fileId}`);
|
|
76
|
+
if (error.code === 403) throw new UserError('Permission denied. Check that the file is shared with this account.');
|
|
77
|
+
throw new UserError(`Failed to get file info: ${error.message}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
log.info(`File "${fileMeta.name}" is ${fileMeta.mimeType}`);
|
|
81
|
+
trackRead(args.fileId, fileMeta.modifiedTime);
|
|
82
|
+
|
|
83
|
+
// 2. Route based on MIME type
|
|
84
|
+
if (fileMeta.mimeType === GOOGLE_DOC) {
|
|
85
|
+
return await readGoogleDoc(args, fileMeta, log);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (fileMeta.mimeType === GOOGLE_SHEET) {
|
|
89
|
+
return await readGoogleSheet(args, fileMeta, log);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Try reading as docx/pdf
|
|
93
|
+
const text = await tryReadBinaryFile(drive, args.fileId, fileMeta, log);
|
|
94
|
+
if (text !== null) {
|
|
95
|
+
return text;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Unsupported type
|
|
99
|
+
const capInfo = CAPABILITIES.other;
|
|
100
|
+
capInfo.fileType = fileMeta.mimeType;
|
|
101
|
+
return JSON.stringify({
|
|
102
|
+
file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
|
|
103
|
+
...capInfo,
|
|
104
|
+
content: null,
|
|
105
|
+
}, null, 2);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readGoogleDoc(args, fileMeta, log) {
|
|
111
|
+
const docs = await getDocsClient();
|
|
112
|
+
const res = await docs.documents.get({
|
|
113
|
+
documentId: args.fileId,
|
|
114
|
+
fields: args.format === 'text' ? 'body(content(paragraph(elements(textRun(content)))))' : '*',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
let content;
|
|
118
|
+
if (args.format === 'json') {
|
|
119
|
+
content = JSON.stringify(res.data, null, 2);
|
|
120
|
+
} else if (args.format === 'markdown') {
|
|
121
|
+
content = docsJsonToMarkdown(res.data);
|
|
122
|
+
} else {
|
|
123
|
+
// text
|
|
124
|
+
let textContent = '';
|
|
125
|
+
res.data.body?.content?.forEach((element) => {
|
|
126
|
+
if (element.paragraph?.elements) {
|
|
127
|
+
element.paragraph.elements.forEach((pe) => {
|
|
128
|
+
if (pe.textRun?.content) textContent += pe.textRun.content;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (element.table?.tableRows) {
|
|
132
|
+
element.table.tableRows.forEach((row) => {
|
|
133
|
+
row.tableCells?.forEach((cell) => {
|
|
134
|
+
cell.content?.forEach((cellElement) => {
|
|
135
|
+
cellElement.paragraph?.elements?.forEach((pe) => {
|
|
136
|
+
if (pe.textRun?.content) textContent += pe.textRun.content;
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
content = textContent || '(empty document)';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return JSON.stringify({
|
|
147
|
+
file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
|
|
148
|
+
...CAPABILITIES.googleDoc,
|
|
149
|
+
content,
|
|
150
|
+
}, null, 2);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function readGoogleSheet(args, fileMeta, log) {
|
|
154
|
+
if (!args.range) {
|
|
155
|
+
// Return sheet info without data
|
|
156
|
+
const sheets = await getSheetsClient();
|
|
157
|
+
const info = await sheets.spreadsheets.get({
|
|
158
|
+
spreadsheetId: args.fileId,
|
|
159
|
+
fields: 'sheets(properties(sheetId,title,gridProperties))',
|
|
160
|
+
});
|
|
161
|
+
const sheetList = info.data.sheets?.map(s => ({
|
|
162
|
+
title: s.properties.title,
|
|
163
|
+
sheetId: s.properties.sheetId,
|
|
164
|
+
rows: s.properties.gridProperties?.rowCount,
|
|
165
|
+
cols: s.properties.gridProperties?.columnCount,
|
|
166
|
+
}));
|
|
167
|
+
return JSON.stringify({
|
|
168
|
+
file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
|
|
169
|
+
...CAPABILITIES.googleSheet,
|
|
170
|
+
note: 'Provide a range parameter (e.g., "Sheet1!A1:C10") to read data. Available sheets listed below.',
|
|
171
|
+
sheets: sheetList,
|
|
172
|
+
}, null, 2);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const sheets = await getSheetsClient();
|
|
176
|
+
const response = await sheets.spreadsheets.values.get({
|
|
177
|
+
spreadsheetId: args.fileId,
|
|
178
|
+
range: args.range,
|
|
179
|
+
});
|
|
180
|
+
return JSON.stringify({
|
|
181
|
+
file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
|
|
182
|
+
...CAPABILITIES.googleSheet,
|
|
183
|
+
range: args.range,
|
|
184
|
+
values: response.data.values || [],
|
|
185
|
+
}, null, 2);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function tryReadBinaryFile(drive, fileId, fileMeta, log) {
|
|
189
|
+
const { mimeType, name } = fileMeta;
|
|
190
|
+
const isDocxFile = mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || name?.endsWith('.docx');
|
|
191
|
+
const isPdfFile = mimeType === 'application/pdf' || name?.endsWith('.pdf');
|
|
192
|
+
|
|
193
|
+
if (!isDocxFile && !isPdfFile) return null;
|
|
194
|
+
|
|
195
|
+
const buffer = await downloadBuffer(drive, fileId);
|
|
196
|
+
const text = await extractText(buffer, mimeType, name);
|
|
197
|
+
|
|
198
|
+
if (text === null) return null;
|
|
199
|
+
|
|
200
|
+
const capInfo = isDocxFile ? CAPABILITIES.docx : CAPABILITIES.pdf;
|
|
201
|
+
return JSON.stringify({
|
|
202
|
+
file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
|
|
203
|
+
...capInfo,
|
|
204
|
+
content: text,
|
|
205
|
+
}, null, 2);
|
|
206
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { UserError } from 'fastmcp';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getDriveClient } from '../../clients.js';
|
|
4
|
+
import { trackRead } from '../../readTracker.js';
|
|
4
5
|
import mammoth from 'mammoth';
|
|
5
6
|
|
|
6
7
|
function isDocx(mimeType, name) {
|
|
@@ -58,6 +59,7 @@ export function register(server) {
|
|
|
58
59
|
});
|
|
59
60
|
|
|
60
61
|
const { name, mimeType } = meta.data;
|
|
62
|
+
trackRead(fileId);
|
|
61
63
|
const buffer = await downloadBuffer(drive, fileId);
|
|
62
64
|
const text = await extractText(buffer, mimeType, name);
|
|
63
65
|
|
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
|
});
|
|
@@ -130,7 +130,8 @@ export function register(server) {
|
|
|
130
130
|
format.textFormat = textFormat;
|
|
131
131
|
}
|
|
132
132
|
await SheetsHelpers.addConditionalFormatRule(sheets, args.spreadsheetId, gridRanges, args.conditionType, conditionValues, format);
|
|
133
|
-
|
|
133
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
134
|
+
return `${sheetUrl}\nSuccessfully added conditional formatting rule to ${args.ranges.join(', ')}.`;
|
|
134
135
|
}
|
|
135
136
|
catch (error) {
|
|
136
137
|
log.error(`Error adding conditional format rule: ${error.message || error}`);
|
|
@@ -21,7 +21,8 @@ export function register(server) {
|
|
|
21
21
|
if (!addedSheet) {
|
|
22
22
|
throw new UserError('Failed to add sheet - no sheet properties returned.');
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
25
|
+
return `${sheetUrl}\nSuccessfully added sheet "${addedSheet.title}" (Sheet ID: ${addedSheet.sheetId}) to spreadsheet.`;
|
|
25
26
|
}
|
|
26
27
|
catch (error) {
|
|
27
28
|
log.error(`Error adding sheet to spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
@@ -30,7 +30,8 @@ export function register(server) {
|
|
|
30
30
|
const updatedCells = response.updates?.updatedCells || 0;
|
|
31
31
|
const updatedRows = response.updates?.updatedRows || 0;
|
|
32
32
|
const updatedRange = response.updates?.updatedRange || args.range;
|
|
33
|
-
|
|
33
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
34
|
+
return `${sheetUrl}\nSuccessfully appended ${updatedRows} row(s) (${updatedCells} cells) to spreadsheet. Updated range: ${updatedRange}`;
|
|
34
35
|
}
|
|
35
36
|
catch (error) {
|
|
36
37
|
log.error(`Error appending to spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
@@ -32,6 +32,7 @@ export function register(server) {
|
|
|
32
32
|
// Append rows to the table
|
|
33
33
|
const result = await SheetsHelpers.appendToTableHelper(sheets, args.spreadsheetId, table.tableId || '', args.values);
|
|
34
34
|
return JSON.stringify({
|
|
35
|
+
url: `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`,
|
|
35
36
|
tableId: table.tableId,
|
|
36
37
|
name: table.name,
|
|
37
38
|
rowsAppended: result.rowsAppended,
|
|
@@ -54,7 +54,8 @@ export function register(server) {
|
|
|
54
54
|
},
|
|
55
55
|
});
|
|
56
56
|
const rangeDesc = args.columns ? `columns ${args.columns}` : 'all columns';
|
|
57
|
-
|
|
57
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
58
|
+
return `${sheetUrl}\nSuccessfully auto-resized ${rangeDesc} to fit content.`;
|
|
58
59
|
}
|
|
59
60
|
catch (error) {
|
|
60
61
|
log.error(`Error auto-resizing columns: ${error.message || error}`);
|
|
@@ -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}`);
|
|
@@ -40,7 +42,8 @@ export function register(server) {
|
|
|
40
42
|
const totalRows = response.data.totalUpdatedRows || 0;
|
|
41
43
|
const totalColumns = response.data.totalUpdatedColumns || 0;
|
|
42
44
|
const totalSheets = response.data.totalUpdatedSheets || 0;
|
|
43
|
-
|
|
45
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
46
|
+
return `${sheetUrl}\nSuccessfully batch-wrote ${totalCells} cells (${totalRows} rows, ${totalColumns} columns) across ${totalSheets} sheet(s) in ${args.data.length} range(s).`;
|
|
44
47
|
}
|
|
45
48
|
catch (error) {
|
|
46
49
|
log.error(`Error batch writing to spreadsheet ${args.spreadsheetId}: ${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 { guardMutation, trackMutation } from '../../readTracker.js';
|
|
5
6
|
export function register(server) {
|
|
6
7
|
server.addTool({
|
|
7
8
|
name: 'clearRange',
|
|
@@ -13,12 +14,14 @@ 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 {
|
|
19
21
|
const response = await SheetsHelpers.clearRange(sheets, args.spreadsheetId, args.range);
|
|
20
22
|
const clearedRange = response.clearedRange || args.range;
|
|
21
|
-
|
|
23
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
24
|
+
return `${sheetUrl}\nSuccessfully cleared range ${clearedRange}.`;
|
|
22
25
|
}
|
|
23
26
|
catch (error) {
|
|
24
27
|
log.error(`Error clearing range in spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
@@ -46,7 +46,8 @@ export function register(server) {
|
|
|
46
46
|
],
|
|
47
47
|
},
|
|
48
48
|
});
|
|
49
|
-
|
|
49
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
50
|
+
return `${sheetUrl}\nSuccessfully copied formatting from ${args.sourceSheetName}!${args.sourceRange} to ${args.destinationSheetName}!${args.destinationRange}.`;
|
|
50
51
|
}
|
|
51
52
|
catch (error) {
|
|
52
53
|
log.error(`Error copying formatting in spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
@@ -28,7 +28,8 @@ export function register(server) {
|
|
|
28
28
|
],
|
|
29
29
|
},
|
|
30
30
|
});
|
|
31
|
-
|
|
31
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
32
|
+
return `${sheetUrl}\nChart ${args.chartId} deleted successfully.`;
|
|
32
33
|
}
|
|
33
34
|
catch (error) {
|
|
34
35
|
log.error(`Error deleting chart: ${error.message || error}`);
|
|
@@ -30,7 +30,8 @@ export function register(server) {
|
|
|
30
30
|
],
|
|
31
31
|
},
|
|
32
32
|
});
|
|
33
|
-
|
|
33
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
34
|
+
return `${sheetUrl}\nSuccessfully deleted sheet (ID: ${args.sheetId}) from spreadsheet.`;
|
|
34
35
|
}
|
|
35
36
|
catch (error) {
|
|
36
37
|
log.error(`Error deleting sheet in spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
@@ -40,7 +40,8 @@ export function register(server) {
|
|
|
40
40
|
if (!duplicatedSheet) {
|
|
41
41
|
throw new UserError('Failed to duplicate sheet - no sheet properties returned.');
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
44
|
+
return `${sheetUrl}\nSuccessfully duplicated sheet as "${duplicatedSheet.title}" (Sheet ID: ${duplicatedSheet.sheetId}).`;
|
|
44
45
|
}
|
|
45
46
|
catch (error) {
|
|
46
47
|
log.error(`Error duplicating sheet in spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
@@ -93,7 +93,8 @@ export function register(server) {
|
|
|
93
93
|
format.numberFormat = args.numberFormat;
|
|
94
94
|
}
|
|
95
95
|
await SheetsHelpers.formatCells(sheets, args.spreadsheetId, args.range, format);
|
|
96
|
-
|
|
96
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
97
|
+
return `${sheetUrl}\nSuccessfully applied formatting to range "${args.range}".`;
|
|
97
98
|
}
|
|
98
99
|
catch (error) {
|
|
99
100
|
log.error(`Error formatting cells: ${error.message || error}`);
|
|
@@ -45,7 +45,8 @@ export function register(server) {
|
|
|
45
45
|
? 'unfroze columns'
|
|
46
46
|
: `froze left ${args.frozenColumns} column(s)`);
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
49
|
+
return `${sheetUrl}\nSuccessfully ${parts.join(' and ')}.`;
|
|
49
50
|
}
|
|
50
51
|
catch (error) {
|
|
51
52
|
log.error(`Error freezing rows/columns: ${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: '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',
|
|
@@ -49,7 +49,8 @@ export function register(server) {
|
|
|
49
49
|
spreadsheetId: args.spreadsheetId,
|
|
50
50
|
requestBody: { requests },
|
|
51
51
|
});
|
|
52
|
-
|
|
52
|
+
const sheetUrl = `https://docs.google.com/spreadsheets/d/${args.spreadsheetId}/edit`;
|
|
53
|
+
return `${sheetUrl}\nSuccessfully created ${args.groups.length} row group(s).`;
|
|
53
54
|
}
|
|
54
55
|
catch (error) {
|
|
55
56
|
log.error(`Error grouping rows: ${error.message || error}`);
|