gogcli-mcp-sheets 1.0.6

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/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "gogcli-mcp-sheets",
3
+ "version": "1.0.6",
4
+ "description": "Extended Google Sheets MCP server via gogcli — all base tools plus full Sheets support",
5
+ "type": "module",
6
+ "bin": {
7
+ "gogcli-mcp-sheets": "dist/index.js"
8
+ },
9
+ "exports": {
10
+ ".": "./dist/index.js"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc --noEmit && npm run bundle",
14
+ "bundle": "node ../../scripts/bundle.js src/index.ts dist/index.js",
15
+ "typecheck": "tsc --noEmit",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "test:coverage": "vitest run --coverage"
19
+ },
20
+ "dependencies": {
21
+ "gogcli-mcp": "*",
22
+ "@modelcontextprotocol/sdk": "^1.29.0",
23
+ "zod": "^4.3.6"
24
+ },
25
+ "license": "MIT"
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { createBaseServer } from 'gogcli-mcp/lib';
4
+ import { registerExtraSheetsTools } from './tools/sheets-extra.js';
5
+
6
+ const server = createBaseServer({ name: 'gogcli-sheets' });
7
+ registerExtraSheetsTools(server);
8
+
9
+ const transport = new StdioServerTransport();
10
+ await server.connect(transport);
@@ -0,0 +1,340 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { accountParam, runOrDiagnose } from 'gogcli-mcp/lib';
4
+
5
+ export function registerExtraSheetsTools(server: McpServer): void {
6
+ // 1. Add tab
7
+ server.registerTool('gog_sheets_add_tab', {
8
+ description: 'Add a new sheet tab to a spreadsheet.',
9
+ inputSchema: {
10
+ spreadsheetId: z.string().describe('Spreadsheet ID (from the URL)'),
11
+ tabName: z.string().describe('Name for the new tab'),
12
+ account: accountParam,
13
+ },
14
+ }, async ({ spreadsheetId, tabName, account }) => {
15
+ return runOrDiagnose(['sheets', 'add-tab', spreadsheetId, tabName], { account });
16
+ });
17
+
18
+ // 2. Delete tab
19
+ server.registerTool('gog_sheets_delete_tab', {
20
+ description: 'Delete a sheet tab from a spreadsheet.',
21
+ annotations: { destructiveHint: true },
22
+ inputSchema: {
23
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
24
+ tabName: z.string().describe('Name of the tab to delete'),
25
+ account: accountParam,
26
+ },
27
+ }, async ({ spreadsheetId, tabName, account }) => {
28
+ return runOrDiagnose(['sheets', 'delete-tab', spreadsheetId, tabName], { account });
29
+ });
30
+
31
+ // 3. Rename tab
32
+ server.registerTool('gog_sheets_rename_tab', {
33
+ description: 'Rename a sheet tab.',
34
+ annotations: { destructiveHint: true },
35
+ inputSchema: {
36
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
37
+ oldName: z.string().describe('Current tab name'),
38
+ newName: z.string().describe('New tab name'),
39
+ account: accountParam,
40
+ },
41
+ }, async ({ spreadsheetId, oldName, newName, account }) => {
42
+ return runOrDiagnose(['sheets', 'rename-tab', spreadsheetId, oldName, newName], { account });
43
+ });
44
+
45
+ // 4. Copy spreadsheet
46
+ server.registerTool('gog_sheets_copy', {
47
+ description: 'Copy a spreadsheet to a new spreadsheet with the given title.',
48
+ inputSchema: {
49
+ spreadsheetId: z.string().describe('Spreadsheet ID to copy'),
50
+ title: z.string().describe('Title for the new copy'),
51
+ parent: z.string().optional().describe('Parent folder ID to place the copy in'),
52
+ account: accountParam,
53
+ },
54
+ }, async ({ spreadsheetId, title, parent, account }) => {
55
+ const args = ['sheets', 'copy', spreadsheetId, title];
56
+ if (parent) args.push(`--parent=${parent}`);
57
+ return runOrDiagnose(args, { account });
58
+ });
59
+
60
+ // 5. Export spreadsheet
61
+ server.registerTool('gog_sheets_export', {
62
+ description: 'Export a spreadsheet as CSV, TSV, or PDF.',
63
+ annotations: { readOnlyHint: true },
64
+ inputSchema: {
65
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
66
+ format: z.string().optional().describe('Export format: csv, tsv, pdf (default: csv)'),
67
+ out: z.string().optional().describe('Output file path'),
68
+ account: accountParam,
69
+ },
70
+ }, async ({ spreadsheetId, format, out, account }) => {
71
+ const args = ['sheets', 'export', spreadsheetId];
72
+ if (format) args.push(`--format=${format}`);
73
+ if (out) args.push(`--out=${out}`);
74
+ return runOrDiagnose(args, { account });
75
+ });
76
+
77
+ // 6. Freeze rows/columns
78
+ server.registerTool('gog_sheets_freeze', {
79
+ description: 'Freeze rows and/or columns in a sheet.',
80
+ annotations: { destructiveHint: true },
81
+ inputSchema: {
82
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
83
+ rows: z.number().optional().describe('Number of rows to freeze'),
84
+ cols: z.number().optional().describe('Number of columns to freeze'),
85
+ sheet: z.string().optional().describe('Sheet tab name (default: first sheet)'),
86
+ account: accountParam,
87
+ },
88
+ }, async ({ spreadsheetId, rows, cols, sheet, account }) => {
89
+ const args = ['sheets', 'freeze', spreadsheetId];
90
+ if (rows !== undefined) args.push(`--rows=${rows}`);
91
+ if (cols !== undefined) args.push(`--cols=${cols}`);
92
+ if (sheet) args.push(`--sheet=${sheet}`);
93
+ return runOrDiagnose(args, { account });
94
+ });
95
+
96
+ // 7. Insert rows/columns
97
+ server.registerTool('gog_sheets_insert', {
98
+ description: 'Insert rows or columns into a sheet.',
99
+ annotations: { destructiveHint: true },
100
+ inputSchema: {
101
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
102
+ sheet: z.string().describe('Sheet tab name'),
103
+ dimension: z.string().describe('Dimension to insert: ROWS or COLUMNS'),
104
+ start: z.number().describe('Start index (0-based)'),
105
+ count: z.number().optional().describe('Number of rows/columns to insert (default: 1)'),
106
+ after: z.boolean().optional().describe('Insert after the start index instead of before'),
107
+ account: accountParam,
108
+ },
109
+ }, async ({ spreadsheetId, sheet, dimension, start, count, after, account }) => {
110
+ const args = ['sheets', 'insert', spreadsheetId, sheet, dimension, String(start)];
111
+ if (count !== undefined) args.push(`--count=${count}`);
112
+ if (after) args.push('--after');
113
+ return runOrDiagnose(args, { account });
114
+ });
115
+
116
+ // 8. Merge cells
117
+ server.registerTool('gog_sheets_merge', {
118
+ description: 'Merge cells in a range.',
119
+ annotations: { destructiveHint: true },
120
+ inputSchema: {
121
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
122
+ range: z.string().describe('Range to merge (e.g. Sheet1!A1:C3)'),
123
+ type: z.string().optional().describe('Merge type: MERGE_ALL, MERGE_COLUMNS, MERGE_ROWS'),
124
+ account: accountParam,
125
+ },
126
+ }, async ({ spreadsheetId, range, type, account }) => {
127
+ const args = ['sheets', 'merge', spreadsheetId, range];
128
+ if (type) args.push(`--type=${type}`);
129
+ return runOrDiagnose(args, { account });
130
+ });
131
+
132
+ // 9. Unmerge cells
133
+ server.registerTool('gog_sheets_unmerge', {
134
+ description: 'Unmerge previously merged cells in a range.',
135
+ annotations: { destructiveHint: true },
136
+ inputSchema: {
137
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
138
+ range: z.string().describe('Range to unmerge (e.g. Sheet1!A1:C3)'),
139
+ account: accountParam,
140
+ },
141
+ }, async ({ spreadsheetId, range, account }) => {
142
+ return runOrDiagnose(['sheets', 'unmerge', spreadsheetId, range], { account });
143
+ });
144
+
145
+ // 10. Format cells
146
+ server.registerTool('gog_sheets_format', {
147
+ description: 'Apply cell formatting (bold, colors, alignment, etc.) to a range. Pass format as a JSON CellFormat object.',
148
+ annotations: { destructiveHint: true },
149
+ inputSchema: {
150
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
151
+ range: z.string().describe('Range to format (e.g. Sheet1!A1:C3)'),
152
+ formatJson: z.string().describe('JSON CellFormat object (e.g. {"textFormat":{"bold":true}})'),
153
+ formatFields: z.string().optional().describe('Comma-separated field mask (e.g. textFormat.bold,backgroundColor)'),
154
+ account: accountParam,
155
+ },
156
+ }, async ({ spreadsheetId, range, formatJson, formatFields, account }) => {
157
+ const args = ['sheets', 'format', spreadsheetId, range, `--format-json=${formatJson}`];
158
+ if (formatFields) args.push(`--format-fields=${formatFields}`);
159
+ return runOrDiagnose(args, { account });
160
+ });
161
+
162
+ // 11. Number format
163
+ server.registerTool('gog_sheets_number_format', {
164
+ description: 'Set number format on a range (currency, percentage, date, etc.).',
165
+ annotations: { destructiveHint: true },
166
+ inputSchema: {
167
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
168
+ range: z.string().describe('Range to format (e.g. Sheet1!A1:A10)'),
169
+ type: z.string().optional().describe('Format type: NUMBER, CURRENCY, PERCENT, DATE, TIME, SCIENTIFIC, etc.'),
170
+ pattern: z.string().optional().describe('Custom format pattern (e.g. "#,##0.00", "yyyy-mm-dd")'),
171
+ account: accountParam,
172
+ },
173
+ }, async ({ spreadsheetId, range, type, pattern, account }) => {
174
+ const args = ['sheets', 'number-format', spreadsheetId, range];
175
+ if (type) args.push(`--type=${type}`);
176
+ if (pattern) args.push(`--pattern=${pattern}`);
177
+ return runOrDiagnose(args, { account });
178
+ });
179
+
180
+ // 12. Read format
181
+ server.registerTool('gog_sheets_read_format', {
182
+ description: 'Read cell formatting for a range.',
183
+ annotations: { readOnlyHint: true },
184
+ inputSchema: {
185
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
186
+ range: z.string().describe('Range to read formatting from'),
187
+ effective: z.boolean().optional().describe('Return effective (computed) format including inherited styles'),
188
+ account: accountParam,
189
+ },
190
+ }, async ({ spreadsheetId, range, effective, account }) => {
191
+ const args = ['sheets', 'read-format', spreadsheetId, range];
192
+ if (effective) args.push('--effective');
193
+ return runOrDiagnose(args, { account });
194
+ });
195
+
196
+ // 13. Resize columns
197
+ server.registerTool('gog_sheets_resize_columns', {
198
+ description: 'Resize column widths. Use --auto for auto-fit or --width for a specific pixel width.',
199
+ annotations: { destructiveHint: true },
200
+ inputSchema: {
201
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
202
+ columns: z.string().describe('Column range (e.g. A:C, or Sheet1!A:C)'),
203
+ width: z.number().optional().describe('Width in pixels'),
204
+ auto: z.boolean().optional().describe('Auto-fit column width to content'),
205
+ account: accountParam,
206
+ },
207
+ }, async ({ spreadsheetId, columns, width, auto, account }) => {
208
+ const args = ['sheets', 'resize-columns', spreadsheetId, columns];
209
+ if (width !== undefined) args.push(`--width=${width}`);
210
+ if (auto) args.push('--auto');
211
+ return runOrDiagnose(args, { account });
212
+ });
213
+
214
+ // 14. Resize rows
215
+ server.registerTool('gog_sheets_resize_rows', {
216
+ description: 'Resize row heights. Use --auto for auto-fit or --height for a specific pixel height.',
217
+ annotations: { destructiveHint: true },
218
+ inputSchema: {
219
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
220
+ rows: z.string().describe('Row range (e.g. 1:10, or Sheet1!1:10)'),
221
+ height: z.number().optional().describe('Height in pixels'),
222
+ auto: z.boolean().optional().describe('Auto-fit row height to content'),
223
+ account: accountParam,
224
+ },
225
+ }, async ({ spreadsheetId, rows, height, auto, account }) => {
226
+ const args = ['sheets', 'resize-rows', spreadsheetId, rows];
227
+ if (height !== undefined) args.push(`--height=${height}`);
228
+ if (auto) args.push('--auto');
229
+ return runOrDiagnose(args, { account });
230
+ });
231
+
232
+ // 15. Notes (read)
233
+ server.registerTool('gog_sheets_notes', {
234
+ description: 'Read cell notes in a range.',
235
+ annotations: { readOnlyHint: true },
236
+ inputSchema: {
237
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
238
+ range: z.string().describe('Range to read notes from (e.g. Sheet1!A1:B5)'),
239
+ account: accountParam,
240
+ },
241
+ }, async ({ spreadsheetId, range, account }) => {
242
+ return runOrDiagnose(['sheets', 'notes', spreadsheetId, range], { account });
243
+ });
244
+
245
+ // 16. Update note
246
+ server.registerTool('gog_sheets_update_note', {
247
+ description: 'Add, update, or clear a cell note. Pass an empty string to clear the note.',
248
+ annotations: { destructiveHint: true },
249
+ inputSchema: {
250
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
251
+ range: z.string().describe('Cell or range to set the note on (e.g. Sheet1!A1)'),
252
+ note: z.string().describe('Note text (empty string clears the note)'),
253
+ account: accountParam,
254
+ },
255
+ }, async ({ spreadsheetId, range, note, account }) => {
256
+ return runOrDiagnose(['sheets', 'update-note', spreadsheetId, range, `--note=${note}`], { account });
257
+ });
258
+
259
+ // 17. Links
260
+ server.registerTool('gog_sheets_links', {
261
+ description: 'List hyperlinks in a range.',
262
+ annotations: { readOnlyHint: true },
263
+ inputSchema: {
264
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
265
+ range: z.string().describe('Range to scan for hyperlinks (e.g. Sheet1!A1:Z100)'),
266
+ account: accountParam,
267
+ },
268
+ }, async ({ spreadsheetId, range, account }) => {
269
+ return runOrDiagnose(['sheets', 'links', spreadsheetId, range], { account });
270
+ });
271
+
272
+ // 18. Named ranges list
273
+ server.registerTool('gog_sheets_named_ranges_list', {
274
+ description: 'List all named ranges in a spreadsheet.',
275
+ annotations: { readOnlyHint: true },
276
+ inputSchema: {
277
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
278
+ account: accountParam,
279
+ },
280
+ }, async ({ spreadsheetId, account }) => {
281
+ return runOrDiagnose(['sheets', 'named-ranges', 'list', spreadsheetId], { account });
282
+ });
283
+
284
+ // 19. Named ranges get
285
+ server.registerTool('gog_sheets_named_ranges_get', {
286
+ description: 'Get a named range by name or ID.',
287
+ annotations: { readOnlyHint: true },
288
+ inputSchema: {
289
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
290
+ nameOrId: z.string().describe('Named range name or ID'),
291
+ account: accountParam,
292
+ },
293
+ }, async ({ spreadsheetId, nameOrId, account }) => {
294
+ return runOrDiagnose(['sheets', 'named-ranges', 'get', spreadsheetId, nameOrId], { account });
295
+ });
296
+
297
+ // 20. Named ranges add
298
+ server.registerTool('gog_sheets_named_ranges_add', {
299
+ description: 'Create a new named range.',
300
+ inputSchema: {
301
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
302
+ name: z.string().describe('Name for the range'),
303
+ range: z.string().describe('Cell range (e.g. Sheet1!A1:B10)'),
304
+ account: accountParam,
305
+ },
306
+ }, async ({ spreadsheetId, name, range, account }) => {
307
+ return runOrDiagnose(['sheets', 'named-ranges', 'add', spreadsheetId, name, range], { account });
308
+ });
309
+
310
+ // 21. Named ranges update
311
+ server.registerTool('gog_sheets_named_ranges_update', {
312
+ description: 'Update a named range (change its name, range, or both).',
313
+ annotations: { destructiveHint: true },
314
+ inputSchema: {
315
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
316
+ nameOrId: z.string().describe('Current named range name or ID'),
317
+ name: z.string().optional().describe('New name'),
318
+ range: z.string().optional().describe('New cell range'),
319
+ account: accountParam,
320
+ },
321
+ }, async ({ spreadsheetId, nameOrId, name, range, account }) => {
322
+ const args = ['sheets', 'named-ranges', 'update', spreadsheetId, nameOrId];
323
+ if (name) args.push(`--name=${name}`);
324
+ if (range) args.push(`--range=${range}`);
325
+ return runOrDiagnose(args, { account });
326
+ });
327
+
328
+ // 22. Named ranges delete
329
+ server.registerTool('gog_sheets_named_ranges_delete', {
330
+ description: 'Delete a named range.',
331
+ annotations: { destructiveHint: true },
332
+ inputSchema: {
333
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
334
+ nameOrId: z.string().describe('Named range name or ID to delete'),
335
+ account: accountParam,
336
+ },
337
+ }, async ({ spreadsheetId, nameOrId, account }) => {
338
+ return runOrDiagnose(['sheets', 'named-ranges', 'delete', spreadsheetId, nameOrId], { account });
339
+ });
340
+ }
@@ -0,0 +1,385 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { registerExtraSheetsTools } from '../../src/tools/sheets-extra.js';
4
+ import * as lib from 'gogcli-mcp/lib';
5
+
6
+ vi.mock('gogcli-mcp/lib', async (importOriginal) => {
7
+ const actual = await importOriginal<typeof lib>();
8
+ return {
9
+ ...actual,
10
+ runOrDiagnose: vi.fn(),
11
+ };
12
+ });
13
+
14
+ type ToolHandler = (args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>;
15
+
16
+ function toText(text: string) {
17
+ return { content: [{ type: 'text', text }] };
18
+ }
19
+
20
+ function setupHandlers(): Map<string, ToolHandler> {
21
+ const server = new McpServer({ name: 'test', version: '0.0.0' });
22
+ const handlers = new Map<string, ToolHandler>();
23
+ vi.spyOn(server, 'registerTool').mockImplementation((name, _config, cb) => {
24
+ handlers.set(name, cb as ToolHandler);
25
+ return undefined as never;
26
+ });
27
+ registerExtraSheetsTools(server);
28
+ return handlers;
29
+ }
30
+
31
+ beforeEach(() => vi.clearAllMocks());
32
+
33
+ // 1. add-tab
34
+ describe('gog_sheets_add_tab', () => {
35
+ it('calls runOrDiagnose with correct args', async () => {
36
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
37
+ const handlers = setupHandlers();
38
+ await handlers.get('gog_sheets_add_tab')!({ spreadsheetId: 'sid', tabName: 'NewTab' });
39
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'add-tab', 'sid', 'NewTab'], { account: undefined });
40
+ });
41
+
42
+ it('forwards account', async () => {
43
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
44
+ const handlers = setupHandlers();
45
+ await handlers.get('gog_sheets_add_tab')!({ spreadsheetId: 'sid', tabName: 'T', account: 'a@b.com' });
46
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'add-tab', 'sid', 'T'], { account: 'a@b.com' });
47
+ });
48
+ });
49
+
50
+ // 2. delete-tab
51
+ describe('gog_sheets_delete_tab', () => {
52
+ it('calls runOrDiagnose with correct args', async () => {
53
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
54
+ const handlers = setupHandlers();
55
+ await handlers.get('gog_sheets_delete_tab')!({ spreadsheetId: 'sid', tabName: 'OldTab' });
56
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'delete-tab', 'sid', 'OldTab'], { account: undefined });
57
+ });
58
+ });
59
+
60
+ // 3. rename-tab
61
+ describe('gog_sheets_rename_tab', () => {
62
+ it('calls runOrDiagnose with correct args', async () => {
63
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
64
+ const handlers = setupHandlers();
65
+ await handlers.get('gog_sheets_rename_tab')!({ spreadsheetId: 'sid', oldName: 'Old', newName: 'New' });
66
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'rename-tab', 'sid', 'Old', 'New'], { account: undefined });
67
+ });
68
+ });
69
+
70
+ // 4. copy
71
+ describe('gog_sheets_copy', () => {
72
+ it('calls runOrDiagnose with correct args', async () => {
73
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
74
+ const handlers = setupHandlers();
75
+ await handlers.get('gog_sheets_copy')!({ spreadsheetId: 'sid', title: 'Copy' });
76
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'copy', 'sid', 'Copy'], { account: undefined });
77
+ });
78
+
79
+ it('includes --parent when provided', async () => {
80
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
81
+ const handlers = setupHandlers();
82
+ await handlers.get('gog_sheets_copy')!({ spreadsheetId: 'sid', title: 'Copy', parent: 'folderId' });
83
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'copy', 'sid', 'Copy', '--parent=folderId'], { account: undefined });
84
+ });
85
+ });
86
+
87
+ // 5. export
88
+ describe('gog_sheets_export', () => {
89
+ it('calls runOrDiagnose with correct args', async () => {
90
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
91
+ const handlers = setupHandlers();
92
+ await handlers.get('gog_sheets_export')!({ spreadsheetId: 'sid' });
93
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'export', 'sid'], { account: undefined });
94
+ });
95
+
96
+ it('includes --format and --out when provided', async () => {
97
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
98
+ const handlers = setupHandlers();
99
+ await handlers.get('gog_sheets_export')!({ spreadsheetId: 'sid', format: 'pdf', out: '/tmp/out.pdf' });
100
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'export', 'sid', '--format=pdf', '--out=/tmp/out.pdf'], { account: undefined });
101
+ });
102
+ });
103
+
104
+ // 6. freeze
105
+ describe('gog_sheets_freeze', () => {
106
+ it('calls runOrDiagnose with no optional flags', async () => {
107
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
108
+ const handlers = setupHandlers();
109
+ await handlers.get('gog_sheets_freeze')!({ spreadsheetId: 'sid' });
110
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'freeze', 'sid'], { account: undefined });
111
+ });
112
+
113
+ it('includes --rows, --cols, --sheet when provided', async () => {
114
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
115
+ const handlers = setupHandlers();
116
+ await handlers.get('gog_sheets_freeze')!({ spreadsheetId: 'sid', rows: 1, cols: 2, sheet: 'Data' });
117
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'freeze', 'sid', '--rows=1', '--cols=2', '--sheet=Data'], { account: undefined });
118
+ });
119
+
120
+ it('includes --rows=0 when rows is 0', async () => {
121
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
122
+ const handlers = setupHandlers();
123
+ await handlers.get('gog_sheets_freeze')!({ spreadsheetId: 'sid', rows: 0 });
124
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'freeze', 'sid', '--rows=0'], { account: undefined });
125
+ });
126
+ });
127
+
128
+ // 7. insert
129
+ describe('gog_sheets_insert', () => {
130
+ it('calls runOrDiagnose with required args', async () => {
131
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
132
+ const handlers = setupHandlers();
133
+ await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'Sheet1', dimension: 'ROWS', start: 5 });
134
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'Sheet1', 'ROWS', '5'], { account: undefined });
135
+ });
136
+
137
+ it('includes --count and --after when provided', async () => {
138
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
139
+ const handlers = setupHandlers();
140
+ await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'S1', dimension: 'COLUMNS', start: 0, count: 3, after: true });
141
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'S1', 'COLUMNS', '0', '--count=3', '--after'], { account: undefined });
142
+ });
143
+
144
+ it('includes --count=0 when count is 0', async () => {
145
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
146
+ const handlers = setupHandlers();
147
+ await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 0, count: 0 });
148
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'S1', 'ROWS', '0', '--count=0'], { account: undefined });
149
+ });
150
+
151
+ it('omits --after when false', async () => {
152
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
153
+ const handlers = setupHandlers();
154
+ await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 0, after: false });
155
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'S1', 'ROWS', '0'], { account: undefined });
156
+ });
157
+ });
158
+
159
+ // 8. merge
160
+ describe('gog_sheets_merge', () => {
161
+ it('calls runOrDiagnose with correct args', async () => {
162
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
163
+ const handlers = setupHandlers();
164
+ await handlers.get('gog_sheets_merge')!({ spreadsheetId: 'sid', range: 'A1:C3' });
165
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'merge', 'sid', 'A1:C3'], { account: undefined });
166
+ });
167
+
168
+ it('includes --type when provided', async () => {
169
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
170
+ const handlers = setupHandlers();
171
+ await handlers.get('gog_sheets_merge')!({ spreadsheetId: 'sid', range: 'A1:C3', type: 'MERGE_COLUMNS' });
172
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'merge', 'sid', 'A1:C3', '--type=MERGE_COLUMNS'], { account: undefined });
173
+ });
174
+ });
175
+
176
+ // 9. unmerge
177
+ describe('gog_sheets_unmerge', () => {
178
+ it('calls runOrDiagnose with correct args', async () => {
179
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
180
+ const handlers = setupHandlers();
181
+ await handlers.get('gog_sheets_unmerge')!({ spreadsheetId: 'sid', range: 'A1:C3' });
182
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'unmerge', 'sid', 'A1:C3'], { account: undefined });
183
+ });
184
+ });
185
+
186
+ // 10. format
187
+ describe('gog_sheets_format', () => {
188
+ it('calls runOrDiagnose with correct args', async () => {
189
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
190
+ const handlers = setupHandlers();
191
+ const fj = '{"textFormat":{"bold":true}}';
192
+ await handlers.get('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1:B2', formatJson: fj });
193
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'format', 'sid', 'A1:B2', `--format-json=${fj}`], { account: undefined });
194
+ });
195
+
196
+ it('includes --format-fields when provided', async () => {
197
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
198
+ const handlers = setupHandlers();
199
+ await handlers.get('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1', formatJson: '{}', formatFields: 'textFormat.bold' });
200
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'format', 'sid', 'A1', '--format-json={}', '--format-fields=textFormat.bold'], { account: undefined });
201
+ });
202
+ });
203
+
204
+ // 11. number-format
205
+ describe('gog_sheets_number_format', () => {
206
+ it('calls runOrDiagnose with correct args', async () => {
207
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
208
+ const handlers = setupHandlers();
209
+ await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A10' });
210
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'number-format', 'sid', 'A1:A10'], { account: undefined });
211
+ });
212
+
213
+ it('includes --type and --pattern when provided', async () => {
214
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
215
+ const handlers = setupHandlers();
216
+ await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1', type: 'CURRENCY', pattern: '#,##0.00' });
217
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'number-format', 'sid', 'A1', '--type=CURRENCY', '--pattern=#,##0.00'], { account: undefined });
218
+ });
219
+ });
220
+
221
+ // 12. read-format
222
+ describe('gog_sheets_read_format', () => {
223
+ it('calls runOrDiagnose with correct args', async () => {
224
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
225
+ const handlers = setupHandlers();
226
+ await handlers.get('gog_sheets_read_format')!({ spreadsheetId: 'sid', range: 'A1:B2' });
227
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'read-format', 'sid', 'A1:B2'], { account: undefined });
228
+ });
229
+
230
+ it('includes --effective when true', async () => {
231
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
232
+ const handlers = setupHandlers();
233
+ await handlers.get('gog_sheets_read_format')!({ spreadsheetId: 'sid', range: 'A1', effective: true });
234
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'read-format', 'sid', 'A1', '--effective'], { account: undefined });
235
+ });
236
+
237
+ it('omits --effective when false', async () => {
238
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
239
+ const handlers = setupHandlers();
240
+ await handlers.get('gog_sheets_read_format')!({ spreadsheetId: 'sid', range: 'A1', effective: false });
241
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'read-format', 'sid', 'A1'], { account: undefined });
242
+ });
243
+ });
244
+
245
+ // 13. resize-columns
246
+ describe('gog_sheets_resize_columns', () => {
247
+ it('calls runOrDiagnose with correct args', async () => {
248
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
249
+ const handlers = setupHandlers();
250
+ await handlers.get('gog_sheets_resize_columns')!({ spreadsheetId: 'sid', columns: 'A:C' });
251
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'resize-columns', 'sid', 'A:C'], { account: undefined });
252
+ });
253
+
254
+ it('includes --width and --auto when provided', async () => {
255
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
256
+ const handlers = setupHandlers();
257
+ await handlers.get('gog_sheets_resize_columns')!({ spreadsheetId: 'sid', columns: 'A:C', width: 200, auto: true });
258
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'resize-columns', 'sid', 'A:C', '--width=200', '--auto'], { account: undefined });
259
+ });
260
+
261
+ it('includes --width=0 when width is 0', async () => {
262
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
263
+ const handlers = setupHandlers();
264
+ await handlers.get('gog_sheets_resize_columns')!({ spreadsheetId: 'sid', columns: 'A:A', width: 0 });
265
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'resize-columns', 'sid', 'A:A', '--width=0'], { account: undefined });
266
+ });
267
+ });
268
+
269
+ // 14. resize-rows
270
+ describe('gog_sheets_resize_rows', () => {
271
+ it('calls runOrDiagnose with correct args', async () => {
272
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
273
+ const handlers = setupHandlers();
274
+ await handlers.get('gog_sheets_resize_rows')!({ spreadsheetId: 'sid', rows: '1:10' });
275
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'resize-rows', 'sid', '1:10'], { account: undefined });
276
+ });
277
+
278
+ it('includes --height and --auto when provided', async () => {
279
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
280
+ const handlers = setupHandlers();
281
+ await handlers.get('gog_sheets_resize_rows')!({ spreadsheetId: 'sid', rows: '1:5', height: 40, auto: true });
282
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'resize-rows', 'sid', '1:5', '--height=40', '--auto'], { account: undefined });
283
+ });
284
+
285
+ it('includes --height=0 when height is 0', async () => {
286
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
287
+ const handlers = setupHandlers();
288
+ await handlers.get('gog_sheets_resize_rows')!({ spreadsheetId: 'sid', rows: '1:1', height: 0 });
289
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'resize-rows', 'sid', '1:1', '--height=0'], { account: undefined });
290
+ });
291
+ });
292
+
293
+ // 15. notes
294
+ describe('gog_sheets_notes', () => {
295
+ it('calls runOrDiagnose with correct args', async () => {
296
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
297
+ const handlers = setupHandlers();
298
+ await handlers.get('gog_sheets_notes')!({ spreadsheetId: 'sid', range: 'A1:B5' });
299
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'notes', 'sid', 'A1:B5'], { account: undefined });
300
+ });
301
+ });
302
+
303
+ // 16. update-note
304
+ describe('gog_sheets_update_note', () => {
305
+ it('calls runOrDiagnose with correct args', async () => {
306
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
307
+ const handlers = setupHandlers();
308
+ await handlers.get('gog_sheets_update_note')!({ spreadsheetId: 'sid', range: 'A1', note: 'Hello' });
309
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'update-note', 'sid', 'A1', '--note=Hello'], { account: undefined });
310
+ });
311
+
312
+ it('passes empty string to clear note', async () => {
313
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
314
+ const handlers = setupHandlers();
315
+ await handlers.get('gog_sheets_update_note')!({ spreadsheetId: 'sid', range: 'A1', note: '' });
316
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'update-note', 'sid', 'A1', '--note='], { account: undefined });
317
+ });
318
+ });
319
+
320
+ // 17. links
321
+ describe('gog_sheets_links', () => {
322
+ it('calls runOrDiagnose with correct args', async () => {
323
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
324
+ const handlers = setupHandlers();
325
+ await handlers.get('gog_sheets_links')!({ spreadsheetId: 'sid', range: 'A1:Z100' });
326
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'links', 'sid', 'A1:Z100'], { account: undefined });
327
+ });
328
+ });
329
+
330
+ // 18. named-ranges list
331
+ describe('gog_sheets_named_ranges_list', () => {
332
+ it('calls runOrDiagnose with correct args', async () => {
333
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
334
+ const handlers = setupHandlers();
335
+ await handlers.get('gog_sheets_named_ranges_list')!({ spreadsheetId: 'sid' });
336
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'named-ranges', 'list', 'sid'], { account: undefined });
337
+ });
338
+ });
339
+
340
+ // 19. named-ranges get
341
+ describe('gog_sheets_named_ranges_get', () => {
342
+ it('calls runOrDiagnose with correct args', async () => {
343
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
344
+ const handlers = setupHandlers();
345
+ await handlers.get('gog_sheets_named_ranges_get')!({ spreadsheetId: 'sid', nameOrId: 'MyRange' });
346
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'named-ranges', 'get', 'sid', 'MyRange'], { account: undefined });
347
+ });
348
+ });
349
+
350
+ // 20. named-ranges add
351
+ describe('gog_sheets_named_ranges_add', () => {
352
+ it('calls runOrDiagnose with correct args', async () => {
353
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
354
+ const handlers = setupHandlers();
355
+ await handlers.get('gog_sheets_named_ranges_add')!({ spreadsheetId: 'sid', name: 'Totals', range: 'Sheet1!A1:B10' });
356
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'named-ranges', 'add', 'sid', 'Totals', 'Sheet1!A1:B10'], { account: undefined });
357
+ });
358
+ });
359
+
360
+ // 21. named-ranges update
361
+ describe('gog_sheets_named_ranges_update', () => {
362
+ it('calls runOrDiagnose with no optional flags', async () => {
363
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
364
+ const handlers = setupHandlers();
365
+ await handlers.get('gog_sheets_named_ranges_update')!({ spreadsheetId: 'sid', nameOrId: 'MyRange' });
366
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'named-ranges', 'update', 'sid', 'MyRange'], { account: undefined });
367
+ });
368
+
369
+ it('includes --name and --range when provided', async () => {
370
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
371
+ const handlers = setupHandlers();
372
+ await handlers.get('gog_sheets_named_ranges_update')!({ spreadsheetId: 'sid', nameOrId: 'MyRange', name: 'NewName', range: 'Sheet1!C1:D5' });
373
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'named-ranges', 'update', 'sid', 'MyRange', '--name=NewName', '--range=Sheet1!C1:D5'], { account: undefined });
374
+ });
375
+ });
376
+
377
+ // 22. named-ranges delete
378
+ describe('gog_sheets_named_ranges_delete', () => {
379
+ it('calls runOrDiagnose with correct args', async () => {
380
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
381
+ const handlers = setupHandlers();
382
+ await handlers.get('gog_sheets_named_ranges_delete')!({ spreadsheetId: 'sid', nameOrId: 'OldRange' });
383
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'named-ranges', 'delete', 'sid', 'OldRange'], { account: undefined });
384
+ });
385
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "composite": true
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["node_modules", "dist"],
10
+ "references": [{ "path": "../gogcli-mcp" }]
11
+ }
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ coverage: {
6
+ provider: 'v8',
7
+ include: ['src/**/*.ts'],
8
+ exclude: ['src/index.ts'],
9
+ thresholds: {
10
+ lines: 100,
11
+ functions: 100,
12
+ branches: 100,
13
+ statements: 100,
14
+ },
15
+ },
16
+ },
17
+ });