gogcli-mcp 2.7.1 → 2.18.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.
Files changed (46) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/index.js +20220 -19700
  4. package/dist/lib.js +15847 -23331
  5. package/manifest.json +33 -1
  6. package/package.json +7 -6
  7. package/server.json +2 -2
  8. package/src/connector-auth.ts +46 -0
  9. package/src/connector-runtime.ts +177 -0
  10. package/src/index.ts +7 -5
  11. package/src/lib.ts +8 -7
  12. package/src/runner.ts +225 -30
  13. package/src/server.ts +21 -25
  14. package/src/tools/api.ts +65 -0
  15. package/src/tools/auth.ts +112 -15
  16. package/src/tools/calendar.ts +24 -9
  17. package/src/tools/docs.ts +30 -3
  18. package/src/tools/drive.ts +124 -1
  19. package/src/tools/gmail.ts +7 -3
  20. package/src/tools/sheets.ts +6 -3
  21. package/src/tools/slides.ts +9 -4
  22. package/src/tools/tasks.ts +3 -1
  23. package/src/tools/utils.ts +163 -27
  24. package/src/worker.ts +99 -0
  25. package/tests/connector-auth.test.ts +28 -0
  26. package/tests/connector-runtime.test.ts +474 -0
  27. package/tests/runner-file-args-failure.test.ts +94 -0
  28. package/tests/runner-file-args.test.ts +232 -0
  29. package/tests/runner.test.ts +221 -13
  30. package/tests/server.test.ts +28 -28
  31. package/tests/tools/api.test.ts +107 -0
  32. package/tests/tools/auth.test.ts +187 -31
  33. package/tests/tools/calendar.test.ts +115 -52
  34. package/tests/tools/classroom.test.ts +77 -77
  35. package/tests/tools/contacts.test.ts +24 -24
  36. package/tests/tools/docs.test.ts +93 -44
  37. package/tests/tools/drive.test.ts +226 -55
  38. package/tests/tools/gmail.test.ts +61 -28
  39. package/tests/tools/sheets.test.ts +81 -70
  40. package/tests/tools/slides.test.ts +56 -36
  41. package/tests/tools/tasks.test.ts +33 -33
  42. package/tests/tools/utils.test.ts +116 -2
  43. package/tests/worker.test.ts +142 -0
  44. package/tsconfig.json +4 -1
  45. package/vitest.config.ts +33 -2
  46. package/tests/helpers/test-harness.ts +0 -27
@@ -2,27 +2,27 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { registerSheetsTools } from '../../src/tools/sheets.js';
4
4
  import * as runner from '../../src/runner.js';
5
- import { setupHandlers as setupHandlersBase, type ToolHandler } from '../helpers/test-harness.js';
5
+ import { createTestHarness } from '@chrischall/mcp-utils/test';
6
6
 
7
7
  vi.mock('../../src/runner.js');
8
8
 
9
- const setupHandlers = () => setupHandlersBase(registerSheetsTools);
9
+ const setupHandlers = () => createTestHarness(registerSheetsTools);
10
10
 
11
11
  beforeEach(() => vi.clearAllMocks());
12
12
 
13
13
  describe('gog_sheets_get', () => {
14
14
  it('calls run with correct args', async () => {
15
15
  vi.mocked(runner.run).mockResolvedValue('{"values":[["a","b"]]}');
16
- const handlers = setupHandlers();
17
- const result = await handlers.get('gog_sheets_get')!({ spreadsheetId: 'sid', range: 'Sheet1!A1:B2' });
16
+ const harness = await setupHandlers();
17
+ const result = await harness.callTool('gog_sheets_get', { spreadsheetId: 'sid', range: 'Sheet1!A1:B2' });
18
18
  expect(runner.run).toHaveBeenCalledWith(['sheets', 'get', 'sid', 'Sheet1!A1:B2'], { account: undefined });
19
19
  expect(result.content[0].text).toBe('{"values":[["a","b"]]}');
20
20
  });
21
21
 
22
22
  it('forwards account override', async () => {
23
23
  vi.mocked(runner.run).mockResolvedValue('{}');
24
- const handlers = setupHandlers();
25
- await handlers.get('gog_sheets_get')!({ spreadsheetId: 'sid', range: 'A1', account: 'other@gmail.com' });
24
+ const harness = await setupHandlers();
25
+ await harness.callTool('gog_sheets_get', { spreadsheetId: 'sid', range: 'A1', account: 'other@gmail.com' });
26
26
  expect(runner.run).toHaveBeenCalledWith(['sheets', 'get', 'sid', 'A1'], { account: 'other@gmail.com' });
27
27
  });
28
28
 
@@ -30,15 +30,15 @@ describe('gog_sheets_get', () => {
30
30
  vi.mocked(runner.run)
31
31
  .mockRejectedValueOnce(new Error('Spreadsheet not found'))
32
32
  .mockResolvedValueOnce('user@gmail.com');
33
- const handlers = setupHandlers();
34
- const result = await handlers.get('gog_sheets_get')!({ spreadsheetId: 'bad', range: 'A1' });
33
+ const harness = await setupHandlers();
34
+ const result = await harness.callTool('gog_sheets_get', { spreadsheetId: 'bad', range: 'A1' });
35
35
  expect(result.content[0].text).toBe('Error: Spreadsheet not found\n\nConfigured accounts:\nuser@gmail.com');
36
36
  });
37
37
 
38
38
  it('returns plain error text when auth list also fails', async () => {
39
39
  vi.mocked(runner.run).mockRejectedValue(new Error('Spreadsheet not found'));
40
- const handlers = setupHandlers();
41
- const result = await handlers.get('gog_sheets_get')!({ spreadsheetId: 'bad', range: 'A1' });
40
+ const harness = await setupHandlers();
41
+ const result = await harness.callTool('gog_sheets_get', { spreadsheetId: 'bad', range: 'A1' });
42
42
  expect(result.content[0].text).toBe('Error: Spreadsheet not found');
43
43
  });
44
44
 
@@ -46,8 +46,8 @@ describe('gog_sheets_get', () => {
46
46
  vi.mocked(runner.run)
47
47
  .mockRejectedValueOnce('raw error string')
48
48
  .mockRejectedValueOnce(new Error('auth list failed'));
49
- const handlers = setupHandlers();
50
- const result = await handlers.get('gog_sheets_get')!({ spreadsheetId: 'bad', range: 'A1' });
49
+ const harness = await setupHandlers();
50
+ const result = await harness.callTool('gog_sheets_get', { spreadsheetId: 'bad', range: 'A1' });
51
51
  expect(result.content[0].text).toBe('raw error string');
52
52
  });
53
53
  });
@@ -55,9 +55,9 @@ describe('gog_sheets_get', () => {
55
55
  describe('gog_sheets_update', () => {
56
56
  it('passes values via --values-json flag', async () => {
57
57
  vi.mocked(runner.run).mockResolvedValue('{"updatedCells":2}');
58
- const handlers = setupHandlers();
58
+ const harness = await setupHandlers();
59
59
  const values = [['hello', 'world']];
60
- await handlers.get('gog_sheets_update')!({ spreadsheetId: 'sid', range: 'A1:B1', values });
60
+ await harness.callTool('gog_sheets_update', { spreadsheetId: 'sid', range: 'A1:B1', values });
61
61
  expect(runner.run).toHaveBeenCalledWith(
62
62
  ['sheets', 'update', 'sid', 'A1:B1', `--values-json=${JSON.stringify(values)}`],
63
63
  { account: undefined },
@@ -66,9 +66,9 @@ describe('gog_sheets_update', () => {
66
66
 
67
67
  it('preserves non-string cell types (numbers, booleans, nulls, formulas) in --values-json', async () => {
68
68
  vi.mocked(runner.run).mockResolvedValue('{}');
69
- const handlers = setupHandlers();
69
+ const harness = await setupHandlers();
70
70
  const values = [['Sheet', 'A', 'B', 'C'], ['Row', 1, 2.5, '=A2+B2'], ['Bool', true, false, null]];
71
- await handlers.get('gog_sheets_update')!({ spreadsheetId: 'sid', range: 'A1:D3', values });
71
+ await harness.callTool('gog_sheets_update', { spreadsheetId: 'sid', range: 'A1:D3', values });
72
72
  const call = vi.mocked(runner.run).mock.calls[0]!;
73
73
  expect(call[0][4]).toBe(`--values-json=${JSON.stringify(values)}`);
74
74
  // Spot-check the serialized JSON keeps non-string primitives intact (no stringification)
@@ -81,16 +81,16 @@ describe('gog_sheets_update', () => {
81
81
 
82
82
  it('returns error text on failure', async () => {
83
83
  vi.mocked(runner.run).mockRejectedValue(new Error('Update failed'));
84
- const handlers = setupHandlers();
85
- const result = await handlers.get('gog_sheets_update')!({ spreadsheetId: 'bad', range: 'A1', values: [['x']] });
84
+ const harness = await setupHandlers();
85
+ const result = await harness.callTool('gog_sheets_update', { spreadsheetId: 'bad', range: 'A1', values: [['x']] });
86
86
  expect(result.content[0].text).toBe('Error: Update failed');
87
87
  });
88
88
 
89
89
  it('appends --dry-run when dry_run is true', async () => {
90
90
  vi.mocked(runner.run).mockResolvedValue('{"dryRun":true}');
91
- const handlers = setupHandlers();
91
+ const harness = await setupHandlers();
92
92
  const values = [['x']];
93
- await handlers.get('gog_sheets_update')!({ spreadsheetId: 'sid', range: 'A1', values, dry_run: true });
93
+ await harness.callTool('gog_sheets_update', { spreadsheetId: 'sid', range: 'A1', values, dry_run: true });
94
94
  expect(runner.run).toHaveBeenCalledWith(
95
95
  ['sheets', 'update', 'sid', 'A1', `--values-json=${JSON.stringify(values)}`, '--dry-run'],
96
96
  { account: undefined },
@@ -99,17 +99,28 @@ describe('gog_sheets_update', () => {
99
99
 
100
100
  it('omits --dry-run when dry_run is false or unset', async () => {
101
101
  vi.mocked(runner.run).mockResolvedValue('{}');
102
- const handlers = setupHandlers();
103
- await handlers.get('gog_sheets_update')!({ spreadsheetId: 'sid', range: 'A1', values: [['x']], dry_run: false });
102
+ const harness = await setupHandlers();
103
+ await harness.callTool('gog_sheets_update', { spreadsheetId: 'sid', range: 'A1', values: [['x']], dry_run: false });
104
104
  expect(vi.mocked(runner.run).mock.calls[0]![0]).not.toContain('--dry-run');
105
105
  });
106
+
107
+ it('appends --fail-on-formula-error when fail_on_formula_error is true', async () => {
108
+ vi.mocked(runner.run).mockResolvedValue('{}');
109
+ const harness = await setupHandlers();
110
+ const values = [['=1+1']];
111
+ await harness.callTool('gog_sheets_update', { spreadsheetId: 'sid', range: 'A1', values, fail_on_formula_error: true });
112
+ expect(runner.run).toHaveBeenCalledWith(
113
+ ['sheets', 'update', 'sid', 'A1', `--values-json=${JSON.stringify(values)}`, '--fail-on-formula-error'],
114
+ { account: undefined },
115
+ );
116
+ });
106
117
  });
107
118
 
108
119
  describe('gog_sheets_update fail_if_not_empty guard', () => {
109
120
  it('aborts the write when the target range already holds data', async () => {
110
121
  vi.mocked(runner.run).mockResolvedValueOnce('{"values":[["existing"]]}');
111
- const handlers = setupHandlers();
112
- const result = await handlers.get('gog_sheets_update')!({
122
+ const harness = await setupHandlers();
123
+ const result = await harness.callTool('gog_sheets_update', {
113
124
  spreadsheetId: 'sid', range: 'A1', values: [['new']], fail_if_not_empty: true,
114
125
  });
115
126
  // Only the read happened — no update call.
@@ -124,9 +135,9 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
124
135
  vi.mocked(runner.run)
125
136
  .mockResolvedValueOnce('{}')
126
137
  .mockResolvedValueOnce('{"updatedCells":1}');
127
- const handlers = setupHandlers();
138
+ const harness = await setupHandlers();
128
139
  const values = [['new']];
129
- const result = await handlers.get('gog_sheets_update')!({
140
+ const result = await harness.callTool('gog_sheets_update', {
130
141
  spreadsheetId: 'sid', range: 'A1', values, fail_if_not_empty: true,
131
142
  });
132
143
  expect(runner.run).toHaveBeenCalledTimes(2);
@@ -140,8 +151,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
140
151
  vi.mocked(runner.run)
141
152
  .mockResolvedValueOnce('{}')
142
153
  .mockResolvedValueOnce('{}');
143
- const handlers = setupHandlers();
144
- await handlers.get('gog_sheets_update')!({
154
+ const harness = await setupHandlers();
155
+ await harness.callTool('gog_sheets_update', {
145
156
  spreadsheetId: 'sid', range: 'Sheet1!A1',
146
157
  values: [['a', 'b'], ['c', 'd'], ['e', 'f']], fail_if_not_empty: true,
147
158
  });
@@ -152,8 +163,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
152
163
  vi.mocked(runner.run)
153
164
  .mockRejectedValueOnce(new Error('read boom')) // the verification get
154
165
  .mockResolvedValueOnce('{"accounts":[{"email":"u@x.com"}]}'); // diagnose -> auth list
155
- const handlers = setupHandlers();
156
- const result = await handlers.get('gog_sheets_update')!({
166
+ const harness = await setupHandlers();
167
+ const result = await harness.callTool('gog_sheets_update', {
157
168
  spreadsheetId: 'sid', range: 'A1', values: [['new']], fail_if_not_empty: true,
158
169
  });
159
170
  // get + auth list (for diagnosis), but the update is never attempted
@@ -167,8 +178,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
167
178
  vi.mocked(runner.run)
168
179
  .mockRejectedValueOnce(new Error('Request failed with status 401'))
169
180
  .mockResolvedValueOnce('{"accounts":[{"email":"u@x.com"}]}');
170
- const handlers = setupHandlers();
171
- const result = await handlers.get('gog_sheets_update')!({
181
+ const harness = await setupHandlers();
182
+ const result = await harness.callTool('gog_sheets_update', {
172
183
  spreadsheetId: 'sid', range: 'A1', values: [['x']], fail_if_not_empty: true,
173
184
  });
174
185
  expect(result.content[0].text).toContain('gog_auth_add');
@@ -176,8 +187,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
176
187
 
177
188
  it('aborts when emptiness cannot be verified from unparseable output', async () => {
178
189
  vi.mocked(runner.run).mockResolvedValueOnce('not json');
179
- const handlers = setupHandlers();
180
- const result = await handlers.get('gog_sheets_update')!({
190
+ const harness = await setupHandlers();
191
+ const result = await harness.callTool('gog_sheets_update', {
181
192
  spreadsheetId: 'sid', range: 'A1', values: [['new']], fail_if_not_empty: true,
182
193
  });
183
194
  expect(runner.run).toHaveBeenCalledTimes(1);
@@ -188,8 +199,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
188
199
  vi.mocked(runner.run)
189
200
  .mockResolvedValueOnce('{}')
190
201
  .mockResolvedValueOnce('{"dryRun":true}');
191
- const handlers = setupHandlers();
192
- await handlers.get('gog_sheets_update')!({
202
+ const harness = await setupHandlers();
203
+ await harness.callTool('gog_sheets_update', {
193
204
  spreadsheetId: 'sid', range: 'A1', values: [['new']], fail_if_not_empty: true, dry_run: true,
194
205
  });
195
206
  expect(vi.mocked(runner.run).mock.calls[1]![0]).toContain('--dry-run');
@@ -197,8 +208,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
197
208
 
198
209
  it('skips the guard read when values has no rows', async () => {
199
210
  vi.mocked(runner.run).mockResolvedValue('{}');
200
- const handlers = setupHandlers();
201
- await handlers.get('gog_sheets_update')!({
211
+ const harness = await setupHandlers();
212
+ await harness.callTool('gog_sheets_update', {
202
213
  spreadsheetId: 'sid', range: 'A1', values: [], fail_if_not_empty: true,
203
214
  });
204
215
  expect(runner.run).toHaveBeenCalledTimes(1);
@@ -207,8 +218,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
207
218
 
208
219
  it('skips the guard read when rows have no columns', async () => {
209
220
  vi.mocked(runner.run).mockResolvedValue('{}');
210
- const handlers = setupHandlers();
211
- await handlers.get('gog_sheets_update')!({
221
+ const harness = await setupHandlers();
222
+ await harness.callTool('gog_sheets_update', {
212
223
  spreadsheetId: 'sid', range: 'A1', values: [[]], fail_if_not_empty: true,
213
224
  });
214
225
  expect(runner.run).toHaveBeenCalledTimes(1);
@@ -219,9 +230,9 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
219
230
  describe('gog_sheets_append', () => {
220
231
  it('passes values via --values-json flag', async () => {
221
232
  vi.mocked(runner.run).mockResolvedValue('{"updates":{}}');
222
- const handlers = setupHandlers();
233
+ const harness = await setupHandlers();
223
234
  const values = [['r1c1', 'r1c2'], ['r2c1', 'r2c2']];
224
- await handlers.get('gog_sheets_append')!({ spreadsheetId: 'sid', range: 'Sheet1!A:B', values });
235
+ await harness.callTool('gog_sheets_append', { spreadsheetId: 'sid', range: 'Sheet1!A:B', values });
225
236
  expect(runner.run).toHaveBeenCalledWith(
226
237
  ['sheets', 'append', 'sid', 'Sheet1!A:B', `--values-json=${JSON.stringify(values)}`],
227
238
  { account: undefined },
@@ -230,16 +241,16 @@ describe('gog_sheets_append', () => {
230
241
 
231
242
  it('returns error text on failure', async () => {
232
243
  vi.mocked(runner.run).mockRejectedValue(new Error('Append failed'));
233
- const handlers = setupHandlers();
234
- const result = await handlers.get('gog_sheets_append')!({ spreadsheetId: 'bad', range: 'A1', values: [['x']] });
244
+ const harness = await setupHandlers();
245
+ const result = await harness.callTool('gog_sheets_append', { spreadsheetId: 'bad', range: 'A1', values: [['x']] });
235
246
  expect(result.content[0].text).toBe('Error: Append failed');
236
247
  });
237
248
 
238
249
  it('appends --dry-run when dry_run is true', async () => {
239
250
  vi.mocked(runner.run).mockResolvedValue('{"dryRun":true}');
240
- const handlers = setupHandlers();
251
+ const harness = await setupHandlers();
241
252
  const values = [['x']];
242
- await handlers.get('gog_sheets_append')!({ spreadsheetId: 'sid', range: 'Sheet1!A:A', values, dry_run: true });
253
+ await harness.callTool('gog_sheets_append', { spreadsheetId: 'sid', range: 'Sheet1!A:A', values, dry_run: true });
243
254
  expect(runner.run).toHaveBeenCalledWith(
244
255
  ['sheets', 'append', 'sid', 'Sheet1!A:A', `--values-json=${JSON.stringify(values)}`, '--dry-run'],
245
256
  { account: undefined },
@@ -250,22 +261,22 @@ describe('gog_sheets_append', () => {
250
261
  describe('gog_sheets_clear', () => {
251
262
  it('calls run with correct args', async () => {
252
263
  vi.mocked(runner.run).mockResolvedValue('{}');
253
- const handlers = setupHandlers();
254
- await handlers.get('gog_sheets_clear')!({ spreadsheetId: 'sid', range: 'Sheet1!A1:Z100' });
264
+ const harness = await setupHandlers();
265
+ await harness.callTool('gog_sheets_clear', { spreadsheetId: 'sid', range: 'Sheet1!A1:Z100' });
255
266
  expect(runner.run).toHaveBeenCalledWith(['sheets', 'clear', 'sid', 'Sheet1!A1:Z100'], { account: undefined });
256
267
  });
257
268
 
258
269
  it('returns error text on failure', async () => {
259
270
  vi.mocked(runner.run).mockRejectedValue(new Error('Clear failed'));
260
- const handlers = setupHandlers();
261
- const result = await handlers.get('gog_sheets_clear')!({ spreadsheetId: 'bad', range: 'A1' });
271
+ const harness = await setupHandlers();
272
+ const result = await harness.callTool('gog_sheets_clear', { spreadsheetId: 'bad', range: 'A1' });
262
273
  expect(result.content[0].text).toBe('Error: Clear failed');
263
274
  });
264
275
 
265
276
  it('appends --dry-run when dry_run is true', async () => {
266
277
  vi.mocked(runner.run).mockResolvedValue('{"dryRun":true}');
267
- const handlers = setupHandlers();
268
- await handlers.get('gog_sheets_clear')!({ spreadsheetId: 'sid', range: 'Sheet1!A1:Z100', dry_run: true });
278
+ const harness = await setupHandlers();
279
+ await harness.callTool('gog_sheets_clear', { spreadsheetId: 'sid', range: 'Sheet1!A1:Z100', dry_run: true });
269
280
  expect(runner.run).toHaveBeenCalledWith(
270
281
  ['sheets', 'clear', 'sid', 'Sheet1!A1:Z100', '--dry-run'],
271
282
  { account: undefined },
@@ -287,16 +298,16 @@ describe('gog_sheets_metadata', () => {
287
298
 
288
299
  it('calls run with spreadsheetId only', async () => {
289
300
  vi.mocked(runner.run).mockResolvedValue('{"title":"My Sheet","sheets":[{"title":"Sheet1"}]}');
290
- const handlers = setupHandlers();
291
- const result = await handlers.get('gog_sheets_metadata')!({ spreadsheetId: 'sid' });
301
+ const harness = await setupHandlers();
302
+ const result = await harness.callTool('gog_sheets_metadata', { spreadsheetId: 'sid' });
292
303
  expect(runner.run).toHaveBeenCalledWith(['sheets', 'metadata', 'sid'], { account: undefined });
293
304
  expect(result.content[0].text).toContain('My Sheet');
294
305
  });
295
306
 
296
307
  it('returns error text on failure', async () => {
297
308
  vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
298
- const handlers = setupHandlers();
299
- const result = await handlers.get('gog_sheets_metadata')!({ spreadsheetId: 'bad' });
309
+ const harness = await setupHandlers();
310
+ const result = await harness.callTool('gog_sheets_metadata', { spreadsheetId: 'bad' });
300
311
  expect(result.content[0].text).toBe('Error: Not found');
301
312
  });
302
313
  });
@@ -304,15 +315,15 @@ describe('gog_sheets_metadata', () => {
304
315
  describe('gog_sheets_create', () => {
305
316
  it('calls run with title', async () => {
306
317
  vi.mocked(runner.run).mockResolvedValue('{"spreadsheetId":"newid","title":"Budget 2026"}');
307
- const handlers = setupHandlers();
308
- await handlers.get('gog_sheets_create')!({ title: 'Budget 2026' });
318
+ const harness = await setupHandlers();
319
+ await harness.callTool('gog_sheets_create', { title: 'Budget 2026' });
309
320
  expect(runner.run).toHaveBeenCalledWith(['sheets', 'create', 'Budget 2026'], { account: undefined });
310
321
  });
311
322
 
312
323
  it('returns error text on failure', async () => {
313
324
  vi.mocked(runner.run).mockRejectedValue(new Error('Create failed'));
314
- const handlers = setupHandlers();
315
- const result = await handlers.get('gog_sheets_create')!({ title: 'Bad' });
325
+ const harness = await setupHandlers();
326
+ const result = await harness.callTool('gog_sheets_create', { title: 'Bad' });
316
327
  expect(result.content[0].text).toBe('Error: Create failed');
317
328
  });
318
329
  });
@@ -320,15 +331,15 @@ describe('gog_sheets_create', () => {
320
331
  describe('gog_sheets_find_replace', () => {
321
332
  it('calls run with find and replace args', async () => {
322
333
  vi.mocked(runner.run).mockResolvedValue('{"occurrencesChanged":3}');
323
- const handlers = setupHandlers();
324
- await handlers.get('gog_sheets_find_replace')!({ spreadsheetId: 'sid', find: 'foo', replace: 'bar' });
334
+ const harness = await setupHandlers();
335
+ await harness.callTool('gog_sheets_find_replace', { spreadsheetId: 'sid', find: 'foo', replace: 'bar' });
325
336
  expect(runner.run).toHaveBeenCalledWith(['sheets', 'find-replace', 'sid', 'foo', 'bar'], { account: undefined });
326
337
  });
327
338
 
328
339
  it('returns error text on failure', async () => {
329
340
  vi.mocked(runner.run).mockRejectedValue(new Error('Replace failed'));
330
- const handlers = setupHandlers();
331
- const result = await handlers.get('gog_sheets_find_replace')!({ spreadsheetId: 'bad', find: 'x', replace: 'y' });
341
+ const harness = await setupHandlers();
342
+ const result = await harness.callTool('gog_sheets_find_replace', { spreadsheetId: 'bad', find: 'x', replace: 'y' });
332
343
  expect(result.content[0].text).toBe('Error: Replace failed');
333
344
  });
334
345
  });
@@ -336,8 +347,8 @@ describe('gog_sheets_find_replace', () => {
336
347
  describe('gog_sheets_run', () => {
337
348
  it('passes raw subcommand and args to runner', async () => {
338
349
  vi.mocked(runner.run).mockResolvedValue('{}');
339
- const handlers = setupHandlers();
340
- await handlers.get('gog_sheets_run')!({
350
+ const harness = await setupHandlers();
351
+ await harness.callTool('gog_sheets_run', {
341
352
  subcommand: 'freeze',
342
353
  args: ['sid', '--rows=1'],
343
354
  });
@@ -346,15 +357,15 @@ describe('gog_sheets_run', () => {
346
357
 
347
358
  it('works with empty args array', async () => {
348
359
  vi.mocked(runner.run).mockResolvedValue('{}');
349
- const handlers = setupHandlers();
350
- await handlers.get('gog_sheets_run')!({ subcommand: 'metadata', args: [] });
360
+ const harness = await setupHandlers();
361
+ await harness.callTool('gog_sheets_run', { subcommand: 'metadata', args: [] });
351
362
  expect(runner.run).toHaveBeenCalledWith(['sheets', 'metadata'], { account: undefined });
352
363
  });
353
364
 
354
365
  it('returns error text on failure', async () => {
355
366
  vi.mocked(runner.run).mockRejectedValue(new Error('Run failed'));
356
- const handlers = setupHandlers();
357
- const result = await handlers.get('gog_sheets_run')!({ subcommand: 'freeze', args: [] });
367
+ const harness = await setupHandlers();
368
+ const result = await harness.callTool('gog_sheets_run', { subcommand: 'freeze', args: [] });
358
369
  expect(result.content[0].text).toBe('Error: Run failed');
359
370
  });
360
371
  });
@@ -1,36 +1,46 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { registerSlidesTools } from '../../src/tools/slides.js';
3
3
  import * as runner from '../../src/runner.js';
4
- import { setupHandlers as setupHandlersBase, type ToolHandler } from '../helpers/test-harness.js';
4
+ import { createTestHarness } from '@chrischall/mcp-utils/test';
5
5
 
6
6
  vi.mock('../../src/runner.js');
7
7
 
8
- const setupHandlers = () => setupHandlersBase(registerSlidesTools);
8
+ const setupHandlers = () => createTestHarness(registerSlidesTools);
9
9
 
10
10
  beforeEach(() => vi.clearAllMocks());
11
11
 
12
12
  describe('gog_slides_export', () => {
13
13
  it('calls run with presentationId', async () => {
14
14
  vi.mocked(runner.run).mockResolvedValue('{}');
15
- const handlers = setupHandlers();
16
- await handlers.get('gog_slides_export')!({ presentationId: 'p1' });
15
+ const harness = await setupHandlers();
16
+ await harness.callTool('gog_slides_export', { presentationId: 'p1' });
17
17
  expect(runner.run).toHaveBeenCalledWith(['slides', 'export', 'p1'], { account: undefined });
18
18
  });
19
19
 
20
20
  it('passes --out and --format when provided', async () => {
21
21
  vi.mocked(runner.run).mockResolvedValue('{}');
22
- const handlers = setupHandlers();
23
- await handlers.get('gog_slides_export')!({ presentationId: 'p1', out: '/tmp/deck.pdf', format: 'pdf' });
22
+ const harness = await setupHandlers();
23
+ await harness.callTool('gog_slides_export', { presentationId: 'p1', out: '/tmp/deck.pdf', format: 'pdf' });
24
24
  expect(runner.run).toHaveBeenCalledWith(
25
25
  ['slides', 'export', 'p1', '--out=/tmp/deck.pdf', '--format=pdf'],
26
26
  { account: undefined },
27
27
  );
28
28
  });
29
29
 
30
+ it('passes --overwrite when requested', async () => {
31
+ vi.mocked(runner.run).mockResolvedValue('{}');
32
+ const harness = await setupHandlers();
33
+ await harness.callTool('gog_slides_export', { presentationId: 'p1', out: '/tmp/deck.pptx', overwrite: true });
34
+ expect(runner.run).toHaveBeenCalledWith(
35
+ ['slides', 'export', 'p1', '--out=/tmp/deck.pptx', '--overwrite'],
36
+ { account: undefined },
37
+ );
38
+ });
39
+
30
40
  it('returns error text on failure', async () => {
31
41
  vi.mocked(runner.run).mockRejectedValue(new Error('Export failed'));
32
- const handlers = setupHandlers();
33
- const result = await handlers.get('gog_slides_export')!({ presentationId: 'bad' });
42
+ const harness = await setupHandlers();
43
+ const result = await harness.callTool('gog_slides_export', { presentationId: 'bad' });
34
44
  expect(result.content[0].text).toBe('Error: Export failed');
35
45
  });
36
46
  });
@@ -38,15 +48,15 @@ describe('gog_slides_export', () => {
38
48
  describe('gog_slides_info', () => {
39
49
  it('calls run with presentationId', async () => {
40
50
  vi.mocked(runner.run).mockResolvedValue('{}');
41
- const handlers = setupHandlers();
42
- await handlers.get('gog_slides_info')!({ presentationId: 'p1' });
51
+ const harness = await setupHandlers();
52
+ await harness.callTool('gog_slides_info', { presentationId: 'p1' });
43
53
  expect(runner.run).toHaveBeenCalledWith(['slides', 'info', 'p1'], { account: undefined });
44
54
  });
45
55
 
46
56
  it('returns error text on failure', async () => {
47
57
  vi.mocked(runner.run).mockRejectedValue(new Error('Info failed'));
48
- const handlers = setupHandlers();
49
- const result = await handlers.get('gog_slides_info')!({ presentationId: 'bad' });
58
+ const harness = await setupHandlers();
59
+ const result = await harness.callTool('gog_slides_info', { presentationId: 'bad' });
50
60
  expect(result.content[0].text).toBe('Error: Info failed');
51
61
  });
52
62
  });
@@ -54,15 +64,15 @@ describe('gog_slides_info', () => {
54
64
  describe('gog_slides_create', () => {
55
65
  it('calls run with title only', async () => {
56
66
  vi.mocked(runner.run).mockResolvedValue('{}');
57
- const handlers = setupHandlers();
58
- await handlers.get('gog_slides_create')!({ title: 'Deck' });
67
+ const harness = await setupHandlers();
68
+ await harness.callTool('gog_slides_create', { title: 'Deck' });
59
69
  expect(runner.run).toHaveBeenCalledWith(['slides', 'create', 'Deck'], { account: undefined });
60
70
  });
61
71
 
62
72
  it('passes --parent and --template when provided', async () => {
63
73
  vi.mocked(runner.run).mockResolvedValue('{}');
64
- const handlers = setupHandlers();
65
- await handlers.get('gog_slides_create')!({ title: 'Deck', parent: 'folder1', template: 'tpl1' });
74
+ const harness = await setupHandlers();
75
+ await harness.callTool('gog_slides_create', { title: 'Deck', parent: 'folder1', template: 'tpl1' });
66
76
  expect(runner.run).toHaveBeenCalledWith(
67
77
  ['slides', 'create', 'Deck', '--parent=folder1', '--template=tpl1'],
68
78
  { account: undefined },
@@ -71,8 +81,8 @@ describe('gog_slides_create', () => {
71
81
 
72
82
  it('returns error text on failure', async () => {
73
83
  vi.mocked(runner.run).mockRejectedValue(new Error('Create failed'));
74
- const handlers = setupHandlers();
75
- const result = await handlers.get('gog_slides_create')!({ title: 'x' });
84
+ const harness = await setupHandlers();
85
+ const result = await harness.callTool('gog_slides_create', { title: 'x' });
76
86
  expect(result.content[0].text).toBe('Error: Create failed');
77
87
  });
78
88
  });
@@ -80,8 +90,8 @@ describe('gog_slides_create', () => {
80
90
  describe('gog_slides_copy', () => {
81
91
  it('calls run with presentationId and title', async () => {
82
92
  vi.mocked(runner.run).mockResolvedValue('{}');
83
- const handlers = setupHandlers();
84
- await handlers.get('gog_slides_copy')!({ presentationId: 'p1', title: 'Copy' });
93
+ const harness = await setupHandlers();
94
+ await harness.callTool('gog_slides_copy', { presentationId: 'p1', title: 'Copy' });
85
95
  expect(runner.run).toHaveBeenCalledWith(
86
96
  ['slides', 'copy', 'p1', 'Copy'],
87
97
  { account: undefined },
@@ -90,8 +100,8 @@ describe('gog_slides_copy', () => {
90
100
 
91
101
  it('passes --parent when provided', async () => {
92
102
  vi.mocked(runner.run).mockResolvedValue('{}');
93
- const handlers = setupHandlers();
94
- await handlers.get('gog_slides_copy')!({ presentationId: 'p1', title: 'Copy', parent: 'folder1' });
103
+ const harness = await setupHandlers();
104
+ await harness.callTool('gog_slides_copy', { presentationId: 'p1', title: 'Copy', parent: 'folder1' });
95
105
  expect(runner.run).toHaveBeenCalledWith(
96
106
  ['slides', 'copy', 'p1', 'Copy', '--parent=folder1'],
97
107
  { account: undefined },
@@ -100,8 +110,8 @@ describe('gog_slides_copy', () => {
100
110
 
101
111
  it('returns error text on failure', async () => {
102
112
  vi.mocked(runner.run).mockRejectedValue(new Error('Copy failed'));
103
- const handlers = setupHandlers();
104
- const result = await handlers.get('gog_slides_copy')!({ presentationId: 'p', title: 'x' });
113
+ const harness = await setupHandlers();
114
+ const result = await harness.callTool('gog_slides_copy', { presentationId: 'p', title: 'x' });
105
115
  expect(result.content[0].text).toBe('Error: Copy failed');
106
116
  });
107
117
  });
@@ -109,15 +119,15 @@ describe('gog_slides_copy', () => {
109
119
  describe('gog_slides_list_slides', () => {
110
120
  it('calls run with presentationId', async () => {
111
121
  vi.mocked(runner.run).mockResolvedValue('{}');
112
- const handlers = setupHandlers();
113
- await handlers.get('gog_slides_list_slides')!({ presentationId: 'p1' });
122
+ const harness = await setupHandlers();
123
+ await harness.callTool('gog_slides_list_slides', { presentationId: 'p1' });
114
124
  expect(runner.run).toHaveBeenCalledWith(['slides', 'list-slides', 'p1'], { account: undefined });
115
125
  });
116
126
 
117
127
  it('returns error text on failure', async () => {
118
128
  vi.mocked(runner.run).mockRejectedValue(new Error('List failed'));
119
- const handlers = setupHandlers();
120
- const result = await handlers.get('gog_slides_list_slides')!({ presentationId: 'bad' });
129
+ const harness = await setupHandlers();
130
+ const result = await harness.callTool('gog_slides_list_slides', { presentationId: 'bad' });
121
131
  expect(result.content[0].text).toBe('Error: List failed');
122
132
  });
123
133
  });
@@ -125,18 +135,28 @@ describe('gog_slides_list_slides', () => {
125
135
  describe('gog_slides_read_slide', () => {
126
136
  it('calls run with presentationId and slideId', async () => {
127
137
  vi.mocked(runner.run).mockResolvedValue('{}');
128
- const handlers = setupHandlers();
129
- await handlers.get('gog_slides_read_slide')!({ presentationId: 'p1', slideId: 's1' });
138
+ const harness = await setupHandlers();
139
+ await harness.callTool('gog_slides_read_slide', { presentationId: 'p1', slideId: 's1' });
130
140
  expect(runner.run).toHaveBeenCalledWith(
131
141
  ['slides', 'read-slide', 'p1', 's1'],
132
142
  { account: undefined },
133
143
  );
134
144
  });
135
145
 
146
+ it('adds --detail when detail is true', async () => {
147
+ vi.mocked(runner.run).mockResolvedValue('{}');
148
+ const harness = await setupHandlers();
149
+ await harness.callTool('gog_slides_read_slide', { presentationId: 'p1', slideId: 's1', detail: true });
150
+ expect(runner.run).toHaveBeenCalledWith(
151
+ ['slides', 'read-slide', 'p1', 's1', '--detail'],
152
+ { account: undefined },
153
+ );
154
+ });
155
+
136
156
  it('returns error text on failure', async () => {
137
157
  vi.mocked(runner.run).mockRejectedValue(new Error('Read failed'));
138
- const handlers = setupHandlers();
139
- const result = await handlers.get('gog_slides_read_slide')!({ presentationId: 'p', slideId: 's' });
158
+ const harness = await setupHandlers();
159
+ const result = await harness.callTool('gog_slides_read_slide', { presentationId: 'p', slideId: 's' });
140
160
  expect(result.content[0].text).toBe('Error: Read failed');
141
161
  });
142
162
  });
@@ -144,15 +164,15 @@ describe('gog_slides_read_slide', () => {
144
164
  describe('gog_slides_run', () => {
145
165
  it('passes subcommand and args to runner', async () => {
146
166
  vi.mocked(runner.run).mockResolvedValue('{}');
147
- const handlers = setupHandlers();
148
- await handlers.get('gog_slides_run')!({ subcommand: 'info', args: ['p1'] });
167
+ const harness = await setupHandlers();
168
+ await harness.callTool('gog_slides_run', { subcommand: 'info', args: ['p1'] });
149
169
  expect(runner.run).toHaveBeenCalledWith(['slides', 'info', 'p1'], { account: undefined });
150
170
  });
151
171
 
152
172
  it('returns error text on failure', async () => {
153
173
  vi.mocked(runner.run).mockRejectedValue(new Error('Run failed'));
154
- const handlers = setupHandlers();
155
- const result = await handlers.get('gog_slides_run')!({ subcommand: 'info', args: [] });
174
+ const harness = await setupHandlers();
175
+ const result = await harness.callTool('gog_slides_run', { subcommand: 'info', args: [] });
156
176
  expect(result.content[0].text).toBe('Error: Run failed');
157
177
  });
158
178
  });