google-tools-mcp 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/dist/auth.js +275 -0
- package/dist/cachedToolsList.js +52 -0
- package/dist/clients.js +97 -0
- package/dist/googleDocsApiHelpers.js +883 -0
- package/dist/googleSheetsApiHelpers.js +808 -0
- package/dist/helpers.js +210 -0
- package/dist/index.js +55 -0
- package/dist/logger.js +45 -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 +30 -0
- package/dist/tools/drive/listFolderContents.js +82 -0
- package/dist/tools/drive/listGoogleDocs.js +67 -0
- package/dist/tools/drive/listSharedDrives.js +52 -0
- package/dist/tools/drive/listSharedWithMe.js +97 -0
- package/dist/tools/drive/moveFile.js +54 -0
- package/dist/tools/drive/renameFile.js +39 -0
- package/dist/tools/drive/searchGoogleDocs.js +98 -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/gmail/drafts.js +165 -0
- package/dist/tools/gmail/labels.js +103 -0
- package/dist/tools/gmail/messages.js +448 -0
- package/dist/tools/gmail/settings.js +528 -0
- package/dist/tools/gmail/threads.js +145 -0
- package/dist/tools/index.js +163 -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 +30 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Tool discovery and lazy-loading registration.
|
|
2
|
+
//
|
|
3
|
+
// Only the `load_google_tools` discovery tool is registered at startup.
|
|
4
|
+
// When a category is loaded, its tools are dynamically registered and the
|
|
5
|
+
// client is notified via tools/list_changed so it picks them up.
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
import { getTokenPath } from '../auth.js';
|
|
9
|
+
import { resetClients } from '../clients.js';
|
|
10
|
+
import { logger } from '../logger.js';
|
|
11
|
+
|
|
12
|
+
// --- Category registry ---
|
|
13
|
+
// Maps category names to { loader, description, toolCount }
|
|
14
|
+
const CATEGORIES = {
|
|
15
|
+
files: {
|
|
16
|
+
description: 'Google Drive file management — list, search, create, copy, move, rename, delete files/folders, read file contents (pdf, docx), search within files',
|
|
17
|
+
toolCount: 16,
|
|
18
|
+
async loader(server) {
|
|
19
|
+
const { registerDriveTools } = await import('./drive/index.js');
|
|
20
|
+
const { registerExtrasTools } = await import('./extras/index.js');
|
|
21
|
+
registerDriveTools(server);
|
|
22
|
+
registerExtrasTools(server);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
documents: {
|
|
26
|
+
description: 'Google Docs — read, write, insert text/tables/images, formatting, comments, tabs, markdown conversion',
|
|
27
|
+
toolCount: 23,
|
|
28
|
+
async loader(server) {
|
|
29
|
+
const { registerDocsTools } = await import('./docs/index.js');
|
|
30
|
+
const { registerUtilsTools } = await import('./utils/index.js');
|
|
31
|
+
registerDocsTools(server);
|
|
32
|
+
registerUtilsTools(server);
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
spreadsheets: {
|
|
36
|
+
description: 'Google Sheets — read, write, format cells, charts, tables, conditional formatting, data validation',
|
|
37
|
+
toolCount: 30,
|
|
38
|
+
async loader(server) {
|
|
39
|
+
const { registerSheetsTools } = await import('./sheets/index.js');
|
|
40
|
+
registerSheetsTools(server);
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
email: {
|
|
44
|
+
description: 'Gmail — send, reply, forward, get, list, delete, trash messages, create/update/send drafts, attachments, batch operations',
|
|
45
|
+
toolCount: 19,
|
|
46
|
+
async loader(server) {
|
|
47
|
+
const { register: registerMessages } = await import('./gmail/messages.js');
|
|
48
|
+
const { register: registerDrafts } = await import('./gmail/drafts.js');
|
|
49
|
+
registerMessages(server);
|
|
50
|
+
registerDrafts(server);
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
email_threads: {
|
|
54
|
+
description: 'Gmail threads — get, list, batch get, modify, delete, trash/untrash conversation threads',
|
|
55
|
+
toolCount: 7,
|
|
56
|
+
async loader(server) {
|
|
57
|
+
const { register } = await import('./gmail/threads.js');
|
|
58
|
+
register(server);
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
email_labels: {
|
|
62
|
+
description: 'Gmail labels — create, delete, get, list, update labels for organizing email',
|
|
63
|
+
toolCount: 6,
|
|
64
|
+
async loader(server) {
|
|
65
|
+
const { register } = await import('./gmail/labels.js');
|
|
66
|
+
register(server);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
email_settings: {
|
|
70
|
+
description: 'Gmail settings — auto-forwarding, IMAP, POP, vacation responder, delegates, filters, forwarding addresses, send-as aliases, S/MIME, profile, mailbox watch',
|
|
71
|
+
toolCount: 37,
|
|
72
|
+
async loader(server) {
|
|
73
|
+
const { register } = await import('./gmail/settings.js');
|
|
74
|
+
register(server);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Track which categories have been loaded
|
|
80
|
+
const loadedCategories = new Set();
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Notify the MCP client that the tool list has changed
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
function notifyToolsChanged(server) {
|
|
86
|
+
try {
|
|
87
|
+
const session = server.sessions?.[0];
|
|
88
|
+
if (session?.server?.notification) {
|
|
89
|
+
session.server.notification({ method: 'notifications/tools/list_changed' });
|
|
90
|
+
logger.debug('Sent tools/list_changed notification.');
|
|
91
|
+
} else {
|
|
92
|
+
logger.debug('No session available for tools/list_changed notification.');
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
logger.warn('Failed to send tools/list_changed notification:', err.message);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Public: register the discovery tool (and logout)
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
export function registerAllTools(server) {
|
|
103
|
+
// --- Discovery tool ---
|
|
104
|
+
server.addTool({
|
|
105
|
+
name: 'load_google_tools',
|
|
106
|
+
description:
|
|
107
|
+
'Load Google Workspace tools by category. Call this first before using any Google service.\n\n' +
|
|
108
|
+
'Categories:\n' +
|
|
109
|
+
Object.entries(CATEGORIES)
|
|
110
|
+
.map(([name, { description, toolCount }]) => ` • ${name} (${toolCount} tools) — ${description}`)
|
|
111
|
+
.join('\n') +
|
|
112
|
+
'\n\nYou can load multiple categories at once by passing an array.',
|
|
113
|
+
parameters: z.object({
|
|
114
|
+
categories: z
|
|
115
|
+
.array(z.enum(Object.keys(CATEGORIES)))
|
|
116
|
+
.describe('One or more category names to load'),
|
|
117
|
+
}),
|
|
118
|
+
execute: async ({ categories }, { log }) => {
|
|
119
|
+
const results = [];
|
|
120
|
+
for (const cat of categories) {
|
|
121
|
+
if (loadedCategories.has(cat)) {
|
|
122
|
+
results.push({ category: cat, status: 'already_loaded' });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
log.info(`Loading category: ${cat}`);
|
|
126
|
+
await CATEGORIES[cat].loader(server);
|
|
127
|
+
loadedCategories.add(cat);
|
|
128
|
+
results.push({
|
|
129
|
+
category: cat,
|
|
130
|
+
status: 'loaded',
|
|
131
|
+
toolCount: CATEGORIES[cat].toolCount,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
notifyToolsChanged(server);
|
|
135
|
+
return JSON.stringify({
|
|
136
|
+
loaded: results,
|
|
137
|
+
message: 'Tools are now available. You can call them directly.',
|
|
138
|
+
all_loaded_categories: [...loadedCategories],
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// --- Logout tool (always available) ---
|
|
144
|
+
server.addTool({
|
|
145
|
+
name: 'logout',
|
|
146
|
+
description:
|
|
147
|
+
'Log out of the current Google account by deleting the saved OAuth token. The next tool call will require re-authentication.',
|
|
148
|
+
parameters: z.object({}),
|
|
149
|
+
execute: async () => {
|
|
150
|
+
const tokenPath = getTokenPath();
|
|
151
|
+
try {
|
|
152
|
+
await fs.unlink(tokenPath);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (err.code !== 'ENOENT') throw err;
|
|
155
|
+
}
|
|
156
|
+
resetClients();
|
|
157
|
+
return JSON.stringify({
|
|
158
|
+
success: true,
|
|
159
|
+
message: 'Logged out. The next tool call will require re-authentication.',
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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: 'copyFormatting',
|
|
8
|
+
description: 'Copies formatting (not values) from a source range to a destination range within the same spreadsheet. Copies bold, colors, borders, number formats, etc.',
|
|
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
|
+
sourceSheetName: z.string().min(1).describe('Source sheet/tab name (e.g., "Sheet1").'),
|
|
14
|
+
sourceRange: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe('A1 notation for the source range (e.g., "A1:D10"). Do not include the sheet name — use sourceSheetName instead.'),
|
|
17
|
+
destinationSheetName: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1)
|
|
20
|
+
.describe('Destination sheet/tab name (e.g., "Sheet2").'),
|
|
21
|
+
destinationRange: z
|
|
22
|
+
.string()
|
|
23
|
+
.describe('A1 notation for the destination range (e.g., "A1:D10"). Do not include the sheet name — use destinationSheetName instead.'),
|
|
24
|
+
}),
|
|
25
|
+
execute: async (args, { log }) => {
|
|
26
|
+
const sheets = await getSheetsClient();
|
|
27
|
+
log.info(`Copying formatting from ${args.sourceSheetName}!${args.sourceRange} to ${args.destinationSheetName}!${args.destinationRange} in spreadsheet ${args.spreadsheetId}`);
|
|
28
|
+
try {
|
|
29
|
+
// Resolve sheet names to numeric IDs
|
|
30
|
+
const sourceSheetId = await SheetsHelpers.resolveSheetId(sheets, args.spreadsheetId, args.sourceSheetName);
|
|
31
|
+
const destSheetId = await SheetsHelpers.resolveSheetId(sheets, args.spreadsheetId, args.destinationSheetName);
|
|
32
|
+
// Convert A1 ranges to GridRange objects
|
|
33
|
+
const sourceGridRange = SheetsHelpers.parseA1ToGridRange(args.sourceRange, sourceSheetId);
|
|
34
|
+
const destGridRange = SheetsHelpers.parseA1ToGridRange(args.destinationRange, destSheetId);
|
|
35
|
+
await sheets.spreadsheets.batchUpdate({
|
|
36
|
+
spreadsheetId: args.spreadsheetId,
|
|
37
|
+
requestBody: {
|
|
38
|
+
requests: [
|
|
39
|
+
{
|
|
40
|
+
copyPaste: {
|
|
41
|
+
source: sourceGridRange,
|
|
42
|
+
destination: destGridRange,
|
|
43
|
+
pasteType: 'PASTE_FORMAT',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
return `Successfully copied formatting from ${args.sourceSheetName}!${args.sourceRange} to ${args.destinationSheetName}!${args.destinationRange}.`;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
log.error(`Error copying formatting in spreadsheet ${args.spreadsheetId}: ${error.message || error}`);
|
|
53
|
+
if (error instanceof UserError)
|
|
54
|
+
throw error;
|
|
55
|
+
throw new UserError(`Failed to copy formatting: ${error.message || 'Unknown error'}`);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|