mcp-google-extras 1.0.0
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/LICENSE +15 -0
- package/README.md +80 -0
- package/dist/auth.js +225 -0
- package/dist/cachedToolsList.js +38 -0
- package/dist/clients.js +92 -0
- package/dist/googleDocsApiHelpers.js +883 -0
- package/dist/googleSheetsApiHelpers.js +808 -0
- package/dist/index.js +54 -0
- package/dist/logger.js +58 -0
- package/dist/markdown-transformer/docsToMarkdown.js +259 -0
- package/dist/markdown-transformer/index.js +126 -0
- package/dist/markdown-transformer/markdownToDocs.js +834 -0
- package/dist/tools/docs/addTab.js +92 -0
- package/dist/tools/docs/appendToGoogleDoc.js +81 -0
- package/dist/tools/docs/comments/addComment.js +83 -0
- package/dist/tools/docs/comments/deleteComment.js +30 -0
- package/dist/tools/docs/comments/getComment.js +45 -0
- package/dist/tools/docs/comments/index.js +14 -0
- package/dist/tools/docs/comments/listComments.js +43 -0
- package/dist/tools/docs/comments/replyToComment.js +35 -0
- package/dist/tools/docs/comments/resolveComment.js +55 -0
- package/dist/tools/docs/deleteRange.js +72 -0
- package/dist/tools/docs/findAndReplace.js +54 -0
- package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
- package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
- package/dist/tools/docs/formatting/index.js +6 -0
- package/dist/tools/docs/index.js +38 -0
- package/dist/tools/docs/insertImage.js +122 -0
- package/dist/tools/docs/insertPageBreak.js +58 -0
- package/dist/tools/docs/insertTable.js +53 -0
- package/dist/tools/docs/insertTableWithData.js +135 -0
- package/dist/tools/docs/insertText.js +61 -0
- package/dist/tools/docs/listDocumentTabs.js +60 -0
- package/dist/tools/docs/modifyText.js +158 -0
- package/dist/tools/docs/readGoogleDoc.js +165 -0
- package/dist/tools/docs/renameTab.js +61 -0
- package/dist/tools/drive/copyFile.js +63 -0
- package/dist/tools/drive/createDocument.js +89 -0
- package/dist/tools/drive/createFolder.js +48 -0
- package/dist/tools/drive/createFromTemplate.js +82 -0
- package/dist/tools/drive/deleteFile.js +72 -0
- package/dist/tools/drive/getDocumentInfo.js +48 -0
- package/dist/tools/drive/getFolderInfo.js +48 -0
- package/dist/tools/drive/index.js +26 -0
- package/dist/tools/drive/listFolderContents.js +82 -0
- package/dist/tools/drive/listGoogleDocs.js +67 -0
- package/dist/tools/drive/moveFile.js +54 -0
- package/dist/tools/drive/renameFile.js +39 -0
- package/dist/tools/drive/searchGoogleDocs.js +73 -0
- package/dist/tools/extras/index.js +7 -0
- package/dist/tools/extras/readFile.js +82 -0
- package/dist/tools/extras/searchFileContents.js +81 -0
- package/dist/tools/index.js +15 -0
- package/dist/tools/sheets/addConditionalFormatting.js +143 -0
- package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
- package/dist/tools/sheets/appendSpreadsheetRows.js +43 -0
- package/dist/tools/sheets/appendTableRows.js +50 -0
- package/dist/tools/sheets/autoResizeColumns.js +67 -0
- package/dist/tools/sheets/batchWrite.js +59 -0
- package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
- package/dist/tools/sheets/copyFormatting.js +59 -0
- package/dist/tools/sheets/createSpreadsheet.js +71 -0
- package/dist/tools/sheets/createTable.js +120 -0
- package/dist/tools/sheets/deleteChart.js +41 -0
- package/dist/tools/sheets/deleteSheet.js +43 -0
- package/dist/tools/sheets/deleteTable.js +56 -0
- package/dist/tools/sheets/duplicateSheet.js +53 -0
- package/dist/tools/sheets/formatCells.js +106 -0
- package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
- package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
- package/dist/tools/sheets/getTable.js +48 -0
- package/dist/tools/sheets/groupRows.js +62 -0
- package/dist/tools/sheets/index.js +66 -0
- package/dist/tools/sheets/insertChart.js +225 -0
- package/dist/tools/sheets/listGoogleSheets.js +62 -0
- package/dist/tools/sheets/listTables.js +55 -0
- package/dist/tools/sheets/readCellFormat.js +143 -0
- package/dist/tools/sheets/readSpreadsheet.js +36 -0
- package/dist/tools/sheets/renameSheet.js +48 -0
- package/dist/tools/sheets/setColumnWidths.js +43 -0
- package/dist/tools/sheets/setDropdownValidation.js +51 -0
- package/dist/tools/sheets/ungroupAllRows.js +66 -0
- package/dist/tools/sheets/updateTableRange.js +51 -0
- package/dist/tools/sheets/writeSpreadsheet.js +43 -0
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +93 -0
- package/dist/tools/utils/index.js +6 -0
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +154 -0
- package/dist/types.js +186 -0
- package/package.json +47 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriveClient } from '../../clients.js';
|
|
4
|
+
import mammoth from 'mammoth';
|
|
5
|
+
|
|
6
|
+
function isDocx(mimeType, name) {
|
|
7
|
+
return (
|
|
8
|
+
mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
|
9
|
+
(name && name.endsWith('.docx'))
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isPdf(mimeType, name) {
|
|
14
|
+
return mimeType === 'application/pdf' || (name && name.endsWith('.pdf'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function downloadBuffer(drive, fileId) {
|
|
18
|
+
const response = await drive.files.get(
|
|
19
|
+
{ fileId, alt: 'media', supportsAllDrives: true },
|
|
20
|
+
{ responseType: 'arraybuffer' }
|
|
21
|
+
);
|
|
22
|
+
return Buffer.from(response.data);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function extractText(buffer, mimeType, name) {
|
|
26
|
+
if (isDocx(mimeType, name)) {
|
|
27
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
28
|
+
return result.value;
|
|
29
|
+
}
|
|
30
|
+
if (isPdf(mimeType, name)) {
|
|
31
|
+
const { default: pdfParse } = await import('pdf-parse/lib/pdf-parse.js');
|
|
32
|
+
const result = await pdfParse(buffer);
|
|
33
|
+
return result.text;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { isDocx, isPdf, downloadBuffer };
|
|
39
|
+
|
|
40
|
+
export function register(server) {
|
|
41
|
+
server.addTool({
|
|
42
|
+
name: 'readFile',
|
|
43
|
+
description:
|
|
44
|
+
'Read the full text content of a .docx or .pdf file from Google Drive by file ID. ' +
|
|
45
|
+
'Use this for Word documents and PDFs that cannot be read with readDocument.',
|
|
46
|
+
parameters: z.object({
|
|
47
|
+
fileId: z.string().describe('The Google Drive file ID'),
|
|
48
|
+
}),
|
|
49
|
+
execute: async ({ fileId }, { log }) => {
|
|
50
|
+
const drive = await getDriveClient();
|
|
51
|
+
log.info(`Reading file ${fileId}`);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const meta = await drive.files.get({
|
|
55
|
+
fileId,
|
|
56
|
+
fields: 'name,mimeType',
|
|
57
|
+
supportsAllDrives: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const { name, mimeType } = meta.data;
|
|
61
|
+
const buffer = await downloadBuffer(drive, fileId);
|
|
62
|
+
const text = await extractText(buffer, mimeType, name);
|
|
63
|
+
|
|
64
|
+
if (text === null) {
|
|
65
|
+
throw new UserError(
|
|
66
|
+
`Unsupported file type: ${mimeType} (${name}). Only .docx and .pdf are supported.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return `# ${name}\n\n${text}`;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof UserError) throw error;
|
|
73
|
+
log.error(`Error reading file: ${error.message}`);
|
|
74
|
+
if (error.code === 404)
|
|
75
|
+
throw new UserError(`File not found: ${fileId}`);
|
|
76
|
+
if (error.code === 403)
|
|
77
|
+
throw new UserError('Permission denied. Check that the file is shared with this account.');
|
|
78
|
+
throw new UserError(`Failed to read file: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriveClient } from '../../clients.js';
|
|
4
|
+
import { isDocx, isPdf, downloadBuffer, extractText } from './readFile.js';
|
|
5
|
+
|
|
6
|
+
export function register(server) {
|
|
7
|
+
server.addTool({
|
|
8
|
+
name: 'searchFileContents',
|
|
9
|
+
description:
|
|
10
|
+
'Search Google Drive for files whose content or name matches a query. ' +
|
|
11
|
+
'For .docx and .pdf files, extracts and returns matching text snippets. ' +
|
|
12
|
+
'Use this to find information inside Office documents and PDFs.',
|
|
13
|
+
parameters: z.object({
|
|
14
|
+
query: z.string().describe('Text to search for'),
|
|
15
|
+
maxResults: z
|
|
16
|
+
.number()
|
|
17
|
+
.optional()
|
|
18
|
+
.default(5)
|
|
19
|
+
.describe('Max number of files to search (default 5)'),
|
|
20
|
+
folderId: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Restrict search to a specific folder ID'),
|
|
24
|
+
}),
|
|
25
|
+
execute: async ({ query, maxResults = 5, folderId }, { log }) => {
|
|
26
|
+
const drive = await getDriveClient();
|
|
27
|
+
log.info(`Searching file contents for: "${query}"`);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const escaped = query.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
31
|
+
let q = `fullText contains '${escaped}' and trashed = false`;
|
|
32
|
+
if (folderId) q += ` and '${folderId}' in parents`;
|
|
33
|
+
|
|
34
|
+
const response = await drive.files.list({
|
|
35
|
+
q,
|
|
36
|
+
pageSize: maxResults,
|
|
37
|
+
fields: 'files(id,name,mimeType)',
|
|
38
|
+
supportsAllDrives: true,
|
|
39
|
+
includeItemsFromAllDrives: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const files = response.data.files || [];
|
|
43
|
+
if (files.length === 0) return 'No files found matching that query.';
|
|
44
|
+
|
|
45
|
+
const results = [];
|
|
46
|
+
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const canRead = isDocx(file.mimeType, file.name) || isPdf(file.mimeType, file.name);
|
|
49
|
+
|
|
50
|
+
if (canRead) {
|
|
51
|
+
try {
|
|
52
|
+
const buffer = await downloadBuffer(drive, file.id);
|
|
53
|
+
const text = await extractText(buffer, file.mimeType, file.name);
|
|
54
|
+
|
|
55
|
+
const lowerText = text.toLowerCase();
|
|
56
|
+
const lowerQuery = query.toLowerCase();
|
|
57
|
+
const idx = lowerText.indexOf(lowerQuery);
|
|
58
|
+
const snippet =
|
|
59
|
+
idx !== -1
|
|
60
|
+
? '...' + text.slice(Math.max(0, idx - 150), idx + 300).trim() + '...'
|
|
61
|
+
: text.slice(0, 400).trim() + '...';
|
|
62
|
+
|
|
63
|
+
results.push(`**${file.name}** (ID: \`${file.id}\`)\n\n${snippet}`);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
results.push(`**${file.name}** (ID: \`${file.id}\`)\n\n[Could not read: ${e.message}]`);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
results.push(`**${file.name}** (ID: \`${file.id}\`, type: ${file.mimeType})`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return results.join('\n\n---\n\n');
|
|
73
|
+
} catch (error) {
|
|
74
|
+
log.error(`Error searching files: ${error.message}`);
|
|
75
|
+
if (error.code === 403)
|
|
76
|
+
throw new UserError('Permission denied. Check Drive access.');
|
|
77
|
+
throw new UserError(`Failed to search files: ${error.message}`);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { registerDocsTools } from './docs/index.js';
|
|
2
|
+
import { registerDriveTools } from './drive/index.js';
|
|
3
|
+
import { registerSheetsTools } from './sheets/index.js';
|
|
4
|
+
import { registerUtilsTools } from './utils/index.js';
|
|
5
|
+
import { registerExtrasTools } from './extras/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Registers all tools with the FastMCP server.
|
|
8
|
+
*/
|
|
9
|
+
export function registerAllTools(server) {
|
|
10
|
+
registerDocsTools(server);
|
|
11
|
+
registerDriveTools(server);
|
|
12
|
+
registerSheetsTools(server);
|
|
13
|
+
registerUtilsTools(server);
|
|
14
|
+
registerExtrasTools(server);
|
|
15
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
const ONE_VALUE_CONDITIONS = [
|
|
6
|
+
'NUMBER_GREATER',
|
|
7
|
+
'NUMBER_GREATER_THAN_EQ',
|
|
8
|
+
'NUMBER_LESS',
|
|
9
|
+
'NUMBER_LESS_THAN_EQ',
|
|
10
|
+
'NUMBER_EQ',
|
|
11
|
+
'NUMBER_NOT_EQ',
|
|
12
|
+
'CUSTOM_FORMULA',
|
|
13
|
+
];
|
|
14
|
+
const TWO_VALUE_CONDITIONS = ['NUMBER_BETWEEN', 'NUMBER_NOT_BETWEEN'];
|
|
15
|
+
const NO_VALUE_CONDITIONS = ['BLANK', 'NOT_BLANK'];
|
|
16
|
+
const ALL_CONDITION_TYPES = [
|
|
17
|
+
...ONE_VALUE_CONDITIONS,
|
|
18
|
+
...TWO_VALUE_CONDITIONS,
|
|
19
|
+
...NO_VALUE_CONDITIONS,
|
|
20
|
+
];
|
|
21
|
+
export function register(server) {
|
|
22
|
+
server.addTool({
|
|
23
|
+
name: 'addConditionalFormatting',
|
|
24
|
+
description: 'Adds a conditional formatting rule to one or more ranges in a spreadsheet. Applies a format (background color, bold, text color, etc.) when cells meet a specified condition. Use CUSTOM_FORMULA for complex conditions like "=$A1>$B1". Note: each call appends a new rule — use deleteConditionalFormatting to remove existing rules before re-adding.',
|
|
25
|
+
parameters: z
|
|
26
|
+
.object({
|
|
27
|
+
spreadsheetId: z
|
|
28
|
+
.string()
|
|
29
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
30
|
+
sheetName: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Name of the sheet/tab. Defaults to the first sheet if not provided.'),
|
|
34
|
+
ranges: z
|
|
35
|
+
.array(z.string())
|
|
36
|
+
.min(1)
|
|
37
|
+
.describe('One or more A1 notation ranges the rule applies to (e.g., ["B2:S68"] or ["A1:A10", "C1:C10"]).'),
|
|
38
|
+
conditionType: z
|
|
39
|
+
.enum(ALL_CONDITION_TYPES)
|
|
40
|
+
.describe('The condition type. ' +
|
|
41
|
+
'NUMBER_* types compare cell values numerically. ' +
|
|
42
|
+
'CUSTOM_FORMULA evaluates a formula (e.g., "=$A1>10"). ' +
|
|
43
|
+
'BLANK/NOT_BLANK check whether a cell is empty.'),
|
|
44
|
+
conditionValues: z
|
|
45
|
+
.array(z.string())
|
|
46
|
+
.optional()
|
|
47
|
+
.describe('Values for the condition. ' +
|
|
48
|
+
'Omit or pass [] for BLANK and NOT_BLANK. ' +
|
|
49
|
+
'Pass one value for all NUMBER_* single-operand types and CUSTOM_FORMULA. ' +
|
|
50
|
+
'Pass two values for NUMBER_BETWEEN and NUMBER_NOT_BETWEEN (lower bound first).'),
|
|
51
|
+
backgroundColor: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe('Cell background color as hex (e.g., "#FF9900").'),
|
|
55
|
+
bold: z.boolean().optional().describe('Apply bold text formatting.'),
|
|
56
|
+
italic: z.boolean().optional().describe('Apply italic text formatting.'),
|
|
57
|
+
strikethrough: z.boolean().optional().describe('Apply strikethrough text formatting.'),
|
|
58
|
+
underline: z.boolean().optional().describe('Apply underline text formatting.'),
|
|
59
|
+
foregroundColor: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('Text (foreground) color as hex (e.g., "#FF0000").'),
|
|
63
|
+
fontSize: z.number().min(1).optional().describe('Font size in points.'),
|
|
64
|
+
})
|
|
65
|
+
.refine((data) => data.backgroundColor !== undefined ||
|
|
66
|
+
data.bold !== undefined ||
|
|
67
|
+
data.italic !== undefined ||
|
|
68
|
+
data.strikethrough !== undefined ||
|
|
69
|
+
data.underline !== undefined ||
|
|
70
|
+
data.foregroundColor !== undefined ||
|
|
71
|
+
data.fontSize !== undefined, { message: 'At least one formatting option must be provided.' })
|
|
72
|
+
.refine((data) => {
|
|
73
|
+
const values = data.conditionValues ?? [];
|
|
74
|
+
if (NO_VALUE_CONDITIONS.includes(data.conditionType)) {
|
|
75
|
+
return values.length === 0;
|
|
76
|
+
}
|
|
77
|
+
if (TWO_VALUE_CONDITIONS.includes(data.conditionType)) {
|
|
78
|
+
return values.length === 2;
|
|
79
|
+
}
|
|
80
|
+
return values.length === 1;
|
|
81
|
+
}, (data) => {
|
|
82
|
+
if (NO_VALUE_CONDITIONS.includes(data.conditionType)) {
|
|
83
|
+
return { message: `${data.conditionType} does not accept condition values.` };
|
|
84
|
+
}
|
|
85
|
+
if (TWO_VALUE_CONDITIONS.includes(data.conditionType)) {
|
|
86
|
+
return { message: `${data.conditionType} requires exactly two condition values.` };
|
|
87
|
+
}
|
|
88
|
+
return { message: `${data.conditionType} requires exactly one condition value.` };
|
|
89
|
+
}),
|
|
90
|
+
execute: async (args, { log }) => {
|
|
91
|
+
const sheets = await getSheetsClient();
|
|
92
|
+
log.info(`Adding conditional format rule to spreadsheet ${args.spreadsheetId}`);
|
|
93
|
+
try {
|
|
94
|
+
const sheetId = await SheetsHelpers.resolveSheetId(sheets, args.spreadsheetId, args.sheetName);
|
|
95
|
+
const gridRanges = args.ranges.map((r) => SheetsHelpers.parseA1ToGridRange(r, sheetId));
|
|
96
|
+
const conditionValues = (args.conditionValues ?? []).map((v) => ({
|
|
97
|
+
userEnteredValue: v,
|
|
98
|
+
}));
|
|
99
|
+
const format = {};
|
|
100
|
+
if (args.backgroundColor) {
|
|
101
|
+
const rgb = SheetsHelpers.hexToRgb(args.backgroundColor);
|
|
102
|
+
if (!rgb)
|
|
103
|
+
throw new UserError(`Invalid background color: "${args.backgroundColor}".`);
|
|
104
|
+
format.backgroundColor = rgb;
|
|
105
|
+
}
|
|
106
|
+
const hasTextFormat = args.bold !== undefined ||
|
|
107
|
+
args.italic !== undefined ||
|
|
108
|
+
args.strikethrough !== undefined ||
|
|
109
|
+
args.underline !== undefined ||
|
|
110
|
+
args.fontSize !== undefined ||
|
|
111
|
+
args.foregroundColor !== undefined;
|
|
112
|
+
if (hasTextFormat) {
|
|
113
|
+
const textFormat = {};
|
|
114
|
+
if (args.bold !== undefined)
|
|
115
|
+
textFormat.bold = args.bold;
|
|
116
|
+
if (args.italic !== undefined)
|
|
117
|
+
textFormat.italic = args.italic;
|
|
118
|
+
if (args.strikethrough !== undefined)
|
|
119
|
+
textFormat.strikethrough = args.strikethrough;
|
|
120
|
+
if (args.underline !== undefined)
|
|
121
|
+
textFormat.underline = args.underline;
|
|
122
|
+
if (args.fontSize !== undefined)
|
|
123
|
+
textFormat.fontSize = args.fontSize;
|
|
124
|
+
if (args.foregroundColor) {
|
|
125
|
+
const rgb = SheetsHelpers.hexToRgb(args.foregroundColor);
|
|
126
|
+
if (!rgb)
|
|
127
|
+
throw new UserError(`Invalid foreground color: "${args.foregroundColor}".`);
|
|
128
|
+
textFormat.foregroundColor = rgb;
|
|
129
|
+
}
|
|
130
|
+
format.textFormat = textFormat;
|
|
131
|
+
}
|
|
132
|
+
await SheetsHelpers.addConditionalFormatRule(sheets, args.spreadsheetId, gridRanges, args.conditionType, conditionValues, format);
|
|
133
|
+
return `Successfully added conditional formatting rule to ${args.ranges.join(', ')}.`;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
log.error(`Error adding conditional format rule: ${error.message || error}`);
|
|
137
|
+
if (error instanceof UserError)
|
|
138
|
+
throw error;
|
|
139
|
+
throw new UserError(`Failed to add conditional formatting: ${error.message || 'Unknown error'}`);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'addSheet',
|
|
8
|
+
description: "Adds a new sheet (tab) to an existing spreadsheet. Returns the new sheet's title and ID.",
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
spreadsheetId: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
13
|
+
sheetTitle: z.string().min(1).describe('Title for the new sheet/tab.'),
|
|
14
|
+
}),
|
|
15
|
+
execute: async (args, { log }) => {
|
|
16
|
+
const sheets = await getSheetsClient();
|
|
17
|
+
log.info(`Adding sheet "${args.sheetTitle}" to spreadsheet ${args.spreadsheetId}`);
|
|
18
|
+
try {
|
|
19
|
+
const response = await SheetsHelpers.addSheet(sheets, args.spreadsheetId, args.sheetTitle);
|
|
20
|
+
const addedSheet = response.replies?.[0]?.addSheet?.properties;
|
|
21
|
+
if (!addedSheet) {
|
|
22
|
+
throw new UserError('Failed to add sheet - no sheet properties returned.');
|
|
23
|
+
}
|
|
24
|
+
return `Successfully added sheet "${addedSheet.title}" (Sheet ID: ${addedSheet.sheetId}) to spreadsheet.`;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
log.error(`Error adding sheet to spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
28
|
+
if (error instanceof UserError)
|
|
29
|
+
throw error;
|
|
30
|
+
throw new UserError(`Failed to add sheet: ${error.message || 'Unknown error'}`);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'appendRows',
|
|
8
|
+
description: 'Appends rows to the end of a sheet. Data is added after the last row with content in the specified range.',
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
spreadsheetId: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
13
|
+
range: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe('A1 notation range indicating where to append (e.g., "A1" or "Sheet1!A1"). Data will be appended starting from this range.'),
|
|
16
|
+
values: z
|
|
17
|
+
.array(z.array(z.any()))
|
|
18
|
+
.describe('2D array of values to append. Each inner array represents a row.'),
|
|
19
|
+
valueInputOption: z
|
|
20
|
+
.enum(['RAW', 'USER_ENTERED'])
|
|
21
|
+
.optional()
|
|
22
|
+
.default('USER_ENTERED')
|
|
23
|
+
.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
|
+
execute: async (args, { log }) => {
|
|
26
|
+
const sheets = await getSheetsClient();
|
|
27
|
+
log.info(`Appending rows to spreadsheet ${args.spreadsheetId}, starting at: ${args.range}`);
|
|
28
|
+
try {
|
|
29
|
+
const response = await SheetsHelpers.appendValues(sheets, args.spreadsheetId, args.range, args.values, args.valueInputOption);
|
|
30
|
+
const updatedCells = response.updates?.updatedCells || 0;
|
|
31
|
+
const updatedRows = response.updates?.updatedRows || 0;
|
|
32
|
+
const updatedRange = response.updates?.updatedRange || args.range;
|
|
33
|
+
return `Successfully appended ${updatedRows} row(s) (${updatedCells} cells) to spreadsheet. Updated range: ${updatedRange}`;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
log.error(`Error appending to spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
37
|
+
if (error instanceof UserError)
|
|
38
|
+
throw error;
|
|
39
|
+
throw new UserError(`Failed to append to spreadsheet: ${error.message || 'Unknown error'}`);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'appendTableRows',
|
|
8
|
+
description: 'Appends rows to the end of a table using table-aware insertion. This method respects footers and automatically inserts rows before the footer if one exists.',
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
spreadsheetId: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
13
|
+
tableIdentifier: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe('The table name or table ID to append rows to. Use listTables to see available tables.'),
|
|
16
|
+
values: z
|
|
17
|
+
.array(z.array(z.any()))
|
|
18
|
+
.min(1)
|
|
19
|
+
.describe('2D array of values to append. Each inner array represents a row.'),
|
|
20
|
+
valueInputOption: z
|
|
21
|
+
.enum(['RAW', 'USER_ENTERED'])
|
|
22
|
+
.optional()
|
|
23
|
+
.default('USER_ENTERED')
|
|
24
|
+
.describe('How input data should be interpreted. RAW: values are stored as-is. USER_ENTERED (default): values are parsed as if typed by a user.'),
|
|
25
|
+
}),
|
|
26
|
+
execute: async (args, { log }) => {
|
|
27
|
+
const sheets = await getSheetsClient();
|
|
28
|
+
log.info(`Appending ${args.values.length} rows to table "${args.tableIdentifier}"`);
|
|
29
|
+
try {
|
|
30
|
+
// Resolve the table to get its ID
|
|
31
|
+
const { table } = await SheetsHelpers.resolveTableIdentifier(sheets, args.spreadsheetId, args.tableIdentifier);
|
|
32
|
+
// Append rows to the table
|
|
33
|
+
const result = await SheetsHelpers.appendToTableHelper(sheets, args.spreadsheetId, table.tableId || '', args.values);
|
|
34
|
+
return JSON.stringify({
|
|
35
|
+
tableId: table.tableId,
|
|
36
|
+
name: table.name,
|
|
37
|
+
rowsAppended: result.rowsAppended,
|
|
38
|
+
updatedRange: result.updatedRange,
|
|
39
|
+
message: `Successfully appended ${result.rowsAppended} row(s) to table "${table.name}".`,
|
|
40
|
+
}, null, 2);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
log.error(`Error appending table rows: ${error.message || error}`);
|
|
44
|
+
if (error instanceof UserError)
|
|
45
|
+
throw error;
|
|
46
|
+
throw new UserError(`Failed to append table rows: ${error.message || 'Unknown error'}`);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'autoResizeColumns',
|
|
8
|
+
description: 'Auto-resizes columns in a spreadsheet to fit their content. Optionally restrict to a column range (e.g., "A:S"); defaults to all columns.',
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
spreadsheetId: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
13
|
+
sheetName: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Name of the sheet/tab. Defaults to the first sheet if not provided.'),
|
|
17
|
+
columns: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Column range to resize in A1 notation (e.g., "A:S"). Omit to resize all columns.'),
|
|
21
|
+
}),
|
|
22
|
+
execute: async (args, { log }) => {
|
|
23
|
+
const sheets = await getSheetsClient();
|
|
24
|
+
log.info(`Auto-resizing columns in spreadsheet ${args.spreadsheetId}`);
|
|
25
|
+
try {
|
|
26
|
+
const sheetId = await SheetsHelpers.resolveSheetId(sheets, args.spreadsheetId, args.sheetName);
|
|
27
|
+
const dimensionRange = {
|
|
28
|
+
sheetId,
|
|
29
|
+
dimension: 'COLUMNS',
|
|
30
|
+
};
|
|
31
|
+
if (args.columns) {
|
|
32
|
+
const colonIdx = args.columns.indexOf(':');
|
|
33
|
+
if (colonIdx !== -1) {
|
|
34
|
+
dimensionRange.startIndex = SheetsHelpers.colLettersToIndex(args.columns.slice(0, colonIdx).trim());
|
|
35
|
+
dimensionRange.endIndex =
|
|
36
|
+
SheetsHelpers.colLettersToIndex(args.columns.slice(colonIdx + 1).trim()) + 1;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const idx = SheetsHelpers.colLettersToIndex(args.columns.trim());
|
|
40
|
+
dimensionRange.startIndex = idx;
|
|
41
|
+
dimensionRange.endIndex = idx + 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
await sheets.spreadsheets.batchUpdate({
|
|
45
|
+
spreadsheetId: args.spreadsheetId,
|
|
46
|
+
requestBody: {
|
|
47
|
+
requests: [
|
|
48
|
+
{
|
|
49
|
+
autoResizeDimensions: {
|
|
50
|
+
dimensions: dimensionRange,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const rangeDesc = args.columns ? `columns ${args.columns}` : 'all columns';
|
|
57
|
+
return `Successfully auto-resized ${rangeDesc} to fit content.`;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
log.error(`Error auto-resizing columns: ${error.message || error}`);
|
|
61
|
+
if (error instanceof UserError)
|
|
62
|
+
throw error;
|
|
63
|
+
throw new UserError(`Failed to auto-resize columns: ${error.message || 'Unknown error'}`);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
export function register(server) {
|
|
5
|
+
server.addTool({
|
|
6
|
+
name: 'batchWrite',
|
|
7
|
+
description: 'Writes data to multiple ranges in a single API call. More efficient than multiple separate writeSpreadsheet calls when updating several ranges at once.',
|
|
8
|
+
parameters: z.object({
|
|
9
|
+
spreadsheetId: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
12
|
+
data: z
|
|
13
|
+
.array(z.object({
|
|
14
|
+
range: z.string().describe('A1 notation range (e.g., "Sheet1!A1:B2").'),
|
|
15
|
+
values: z
|
|
16
|
+
.array(z.array(z.any()))
|
|
17
|
+
.describe('2D array of values to write. Each inner array represents a row.'),
|
|
18
|
+
}))
|
|
19
|
+
.min(1)
|
|
20
|
+
.describe('Array of range+values pairs to write in a single batch.'),
|
|
21
|
+
valueInputOption: z
|
|
22
|
+
.enum(['RAW', 'USER_ENTERED'])
|
|
23
|
+
.optional()
|
|
24
|
+
.default('USER_ENTERED')
|
|
25
|
+
.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
|
+
execute: async (args, { log }) => {
|
|
28
|
+
const sheets = await getSheetsClient();
|
|
29
|
+
const rangeNames = args.data.map((d) => d.range).join(', ');
|
|
30
|
+
log.info(`Batch writing to ${args.data.length} range(s) in spreadsheet ${args.spreadsheetId}: ${rangeNames}`);
|
|
31
|
+
try {
|
|
32
|
+
const response = await sheets.spreadsheets.values.batchUpdate({
|
|
33
|
+
spreadsheetId: args.spreadsheetId,
|
|
34
|
+
requestBody: {
|
|
35
|
+
valueInputOption: args.valueInputOption,
|
|
36
|
+
data: args.data.map((d) => ({ range: d.range, values: d.values })),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const totalCells = response.data.totalUpdatedCells || 0;
|
|
40
|
+
const totalRows = response.data.totalUpdatedRows || 0;
|
|
41
|
+
const totalColumns = response.data.totalUpdatedColumns || 0;
|
|
42
|
+
const totalSheets = response.data.totalUpdatedSheets || 0;
|
|
43
|
+
return `Successfully batch-wrote ${totalCells} cells (${totalRows} rows, ${totalColumns} columns) across ${totalSheets} sheet(s) in ${args.data.length} range(s).`;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
log.error(`Error batch writing to spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
47
|
+
if (error instanceof UserError)
|
|
48
|
+
throw error;
|
|
49
|
+
if (error.code === 404) {
|
|
50
|
+
throw new UserError(`Spreadsheet not found (ID: ${args.spreadsheetId}). Check the ID.`);
|
|
51
|
+
}
|
|
52
|
+
if (error.code === 403) {
|
|
53
|
+
throw new UserError(`Permission denied for spreadsheet (ID: ${args.spreadsheetId}). Ensure you have write access.`);
|
|
54
|
+
}
|
|
55
|
+
throw new UserError(`Failed to batch write to spreadsheet: ${error.message || 'Unknown error'}`);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSheetsClient } from '../../clients.js';
|
|
4
|
+
import * as SheetsHelpers from '../../googleSheetsApiHelpers.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'clearRange',
|
|
8
|
+
description: 'Clears all cell values in a range without deleting the cells themselves. Formatting is preserved.',
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
spreadsheetId: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.'),
|
|
13
|
+
range: z.string().describe('A1 notation range to clear (e.g., "A1:B10" or "Sheet1!A1:B10").'),
|
|
14
|
+
}),
|
|
15
|
+
execute: async (args, { log }) => {
|
|
16
|
+
const sheets = await getSheetsClient();
|
|
17
|
+
log.info(`Clearing range ${args.range} in spreadsheet ${args.spreadsheetId}`);
|
|
18
|
+
try {
|
|
19
|
+
const response = await SheetsHelpers.clearRange(sheets, args.spreadsheetId, args.range);
|
|
20
|
+
const clearedRange = response.clearedRange || args.range;
|
|
21
|
+
return `Successfully cleared range ${clearedRange}.`;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
log.error(`Error clearing range in spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
25
|
+
if (error instanceof UserError)
|
|
26
|
+
throw error;
|
|
27
|
+
throw new UserError(`Failed to clear range: ${error.message || 'Unknown error'}`);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|