gogcli-mcp 4.4.0 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +827 -315
- package/dist/lib.js +832 -281
- package/manifest.json +7 -7
- package/package.json +3 -3
- package/server.json +2 -2
- package/src/dispatch-confirmation.ts +102 -115
- package/src/gmail-dispatch-guard.ts +28 -21
- package/src/lib.ts +6 -2
- package/src/send-confirm-token.ts +17 -155
- package/src/tools/api.ts +74 -8
- package/src/tools/appscript.ts +63 -2
- package/src/tools/calendar.ts +83 -13
- package/src/tools/chat.ts +35 -4
- package/src/tools/classroom.ts +254 -25
- package/src/tools/docs.ts +7 -1
- package/src/tools/drive.ts +82 -10
- package/src/tools/gmail.ts +37 -1
- package/src/tools/sheets.ts +16 -1
- package/src/tools/utils.ts +10 -4
- package/tests/gmail-dispatch-guard.test.ts +60 -25
- package/tests/send-confirm-token.test.ts +17 -197
- package/tests/tools/api.test.ts +130 -5
- package/tests/tools/appscript.test.ts +6 -2
- package/tests/tools/dispatch-gates-more.test.ts +447 -0
- package/tests/tools/dispatch-gates.test.ts +74 -13
- package/tests/tools/docs.test.ts +5 -6
- package/tests/tools/drive.test.ts +2 -5
- package/tests/tools/gmail-confirm-token.test.ts +13 -11
- package/tests/tools/run-vets-more.test.ts +269 -0
- package/tests/tools/run-vets.test.ts +158 -4
- package/tests/tools/sheets.test.ts +10 -11
- package/tests/tools/utils.test.ts +38 -41
|
@@ -27,13 +27,13 @@ describe('gog_sheets_get', () => {
|
|
|
27
27
|
expect(runner.run).toHaveBeenCalledWith(['sheets', 'get', pos('sid'), pos('A1')], { account: 'other@gmail.com' });
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
it('appends
|
|
30
|
+
it('appends the account list only on an auth failure', async () => {
|
|
31
31
|
vi.mocked(runner.run)
|
|
32
|
-
.mockRejectedValueOnce(new Error('Spreadsheet not found'))
|
|
33
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
32
|
+
.mockRejectedValueOnce(new Error('Spreadsheet not found'));
|
|
34
33
|
const harness = await setupHandlers();
|
|
35
34
|
const result = await harness.callTool('gog_sheets_get', { spreadsheetId: 'bad', range: 'A1' });
|
|
36
|
-
expect(result.content[0].text).toBe('Error: Spreadsheet not found
|
|
35
|
+
expect(result.content[0].text).toBe('Error: Spreadsheet not found');
|
|
36
|
+
expect(runner.run).toHaveBeenCalledTimes(1);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it('returns plain error text when auth list also fails', async () => {
|
|
@@ -45,8 +45,7 @@ describe('gog_sheets_get', () => {
|
|
|
45
45
|
|
|
46
46
|
it('handles non-Error rejection', async () => {
|
|
47
47
|
vi.mocked(runner.run)
|
|
48
|
-
.mockRejectedValueOnce('raw error string')
|
|
49
|
-
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
48
|
+
.mockRejectedValueOnce('raw error string');
|
|
50
49
|
const harness = await setupHandlers();
|
|
51
50
|
const result = await harness.callTool('gog_sheets_get', { spreadsheetId: 'bad', range: 'A1' });
|
|
52
51
|
expect(result.content[0].text).toBe('raw error string');
|
|
@@ -162,17 +161,17 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
|
162
161
|
|
|
163
162
|
it('aborts without writing and diagnoses the error when the verification read fails', async () => {
|
|
164
163
|
vi.mocked(runner.run)
|
|
165
|
-
.mockRejectedValueOnce(new Error('read boom')) // the verification get
|
|
166
|
-
.mockResolvedValueOnce('{"accounts":[{"email":"u@x.com"}]}'); // diagnose -> auth list
|
|
164
|
+
.mockRejectedValueOnce(new Error('read boom')); // the verification get
|
|
167
165
|
const harness = await setupHandlers();
|
|
168
166
|
const result = await harness.callTool('gog_sheets_update', {
|
|
169
167
|
spreadsheetId: 'sid', range: 'A1', values: [['new']], fail_if_not_empty: true,
|
|
170
168
|
});
|
|
171
|
-
// get
|
|
172
|
-
|
|
169
|
+
// the get only — the update is never attempted, and a non-auth failure
|
|
170
|
+
// does not consult `auth list` (PRIV-1)
|
|
171
|
+
expect(runner.run).toHaveBeenCalledTimes(1);
|
|
173
172
|
expect(vi.mocked(runner.run).mock.calls.some((c) => c[0][1] === 'update')).toBe(false);
|
|
174
173
|
expect(result.content[0].text).toContain('Error: read boom');
|
|
175
|
-
expect(result.content[0].text).toContain('Configured accounts:');
|
|
174
|
+
expect(result.content[0].text).not.toContain('Configured accounts:');
|
|
176
175
|
});
|
|
177
176
|
|
|
178
177
|
it('surfaces the re-auth hint when the verification read fails with an auth error', async () => {
|
|
@@ -217,8 +217,7 @@ describe('runOrDiagnose', () => {
|
|
|
217
217
|
// mask, and retrying it would just spend a second call to fail identically.
|
|
218
218
|
it('does NOT retry an error that is not a rejected mask', async () => {
|
|
219
219
|
vi.mocked(runner.run)
|
|
220
|
-
.mockRejectedValueOnce(new Error('File not found'))
|
|
221
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
220
|
+
.mockRejectedValueOnce(new Error('File not found'));
|
|
222
221
|
const result = await runOrDiagnose(['drive', 'ls'], { fieldsMask: 'files(id)' });
|
|
223
222
|
expect(result.isError).toBe(true);
|
|
224
223
|
expect(runner.run).not.toHaveBeenNthCalledWith(2, ['drive', 'ls'], expect.anything());
|
|
@@ -260,34 +259,35 @@ describe('runOrDiagnose', () => {
|
|
|
260
259
|
expect(result.content[0].text).toBe('user@gmail.com');
|
|
261
260
|
});
|
|
262
261
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
262
|
+
// PRIV-1 (fleet-audit #1012): the account list is PII and only helps when
|
|
263
|
+
// the failure is about WHICH account is signed in. A "Doc not found" told the
|
|
264
|
+
// model every identity on the host for nothing — and cost a `gog auth list`
|
|
265
|
+
// spawn on every error path.
|
|
266
|
+
it('omits the account list on a non-auth failure and does not spawn auth list', async () => {
|
|
267
|
+
vi.mocked(runner.run).mockRejectedValueOnce(new Error('Doc not found'));
|
|
267
268
|
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
268
|
-
expect(result.content[0].text).toBe(
|
|
269
|
-
'Error: Doc not found\n\nConfigured accounts:\nuser@gmail.com',
|
|
270
|
-
);
|
|
269
|
+
expect(result.content[0].text).toBe('Error: Doc not found');
|
|
271
270
|
// mcp-utils errorResult flags diagnosed failures for the client.
|
|
272
271
|
expect(result.isError).toBe(true);
|
|
273
272
|
expect(result.content[0].text).not.toContain('gog_auth_add');
|
|
273
|
+
expect(runner.run).toHaveBeenCalledTimes(1);
|
|
274
274
|
});
|
|
275
275
|
|
|
276
276
|
it('redacts scopes/subject/timestamps from the configured-accounts block', async () => {
|
|
277
277
|
const authListJson = JSON.stringify({
|
|
278
278
|
accounts: [{
|
|
279
|
-
email: '
|
|
279
|
+
email: 'user@example.com',
|
|
280
280
|
subject: '109876543210987654321',
|
|
281
281
|
scopes: ['https://www.googleapis.com/auth/gmail.modify'],
|
|
282
282
|
created_at: '2026-01-02T03:04:05Z',
|
|
283
283
|
}],
|
|
284
284
|
});
|
|
285
285
|
vi.mocked(runner.run)
|
|
286
|
-
.mockRejectedValueOnce(new Error('
|
|
286
|
+
.mockRejectedValueOnce(new Error('Google API error (401 authError): invalid credentials'))
|
|
287
287
|
.mockResolvedValueOnce(authListJson);
|
|
288
288
|
const result = await runOrDiagnose(['gmail', 'drafts', 'delete', pos('r123')], {});
|
|
289
289
|
const text = result.content[0].text;
|
|
290
|
-
expect(text).toContain('Configured accounts:\
|
|
290
|
+
expect(text).toContain('Configured accounts:\nuser@example.com');
|
|
291
291
|
expect(text).not.toContain('scopes');
|
|
292
292
|
expect(text).not.toContain('gmail.modify');
|
|
293
293
|
expect(text).not.toContain('109876543210987654321');
|
|
@@ -296,10 +296,19 @@ describe('runOrDiagnose', () => {
|
|
|
296
296
|
|
|
297
297
|
it('shows (none) when no accounts are configured', async () => {
|
|
298
298
|
vi.mocked(runner.run)
|
|
299
|
-
.mockRejectedValueOnce(new Error('
|
|
299
|
+
.mockRejectedValueOnce(new Error('Request failed with status 401'))
|
|
300
300
|
.mockResolvedValueOnce('{"accounts":[]}');
|
|
301
301
|
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
302
|
-
expect(result.content[0].text).
|
|
302
|
+
expect(result.content[0].text).toContain('Error: Request failed with status 401\n\nConfigured accounts:\n(none)');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('appends the account list on a dead refresh token even when a transient signal is present', async () => {
|
|
306
|
+
vi.mocked(runner.run)
|
|
307
|
+
.mockRejectedValueOnce(new Error('503: token has been expired or revoked'))
|
|
308
|
+
.mockResolvedValueOnce('user@example.com');
|
|
309
|
+
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
310
|
+
expect(result.content[0].text).toContain('Configured accounts:\nuser@example.com');
|
|
311
|
+
expect(result.content[0].text).toContain('invalid_grant');
|
|
303
312
|
});
|
|
304
313
|
|
|
305
314
|
it('appends re-auth hint on 401 error', async () => {
|
|
@@ -345,8 +354,7 @@ describe('runOrDiagnose', () => {
|
|
|
345
354
|
// `invalid_grant` is exempt from this and stays auth (below) — it is the
|
|
346
355
|
// one signal that definitively means the refresh token is dead.
|
|
347
356
|
vi.mocked(runner.run)
|
|
348
|
-
.mockRejectedValueOnce(new Error('429 rateLimitExceeded: the access token expired mid-request, retry'))
|
|
349
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
357
|
+
.mockRejectedValueOnce(new Error('429 rateLimitExceeded: the access token expired mid-request, retry'));
|
|
350
358
|
const result = await runOrDiagnose(['sheets', 'get', pos('A1')], {});
|
|
351
359
|
const text = result.content[0].text as string;
|
|
352
360
|
expect(text).toContain('often transient');
|
|
@@ -374,8 +382,7 @@ describe('runOrDiagnose', () => {
|
|
|
374
382
|
// exercised — and gog's real errors are frequently one long line.
|
|
375
383
|
.mockRejectedValueOnce(
|
|
376
384
|
new Error('page token accepted; the requested export link has expired and must be regenerated'),
|
|
377
|
-
)
|
|
378
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
385
|
+
);
|
|
379
386
|
const result = await runOrDiagnose(['drive', pos('export'), pos('abc')], {});
|
|
380
387
|
expect(result.content[0].text).not.toContain('gog_auth_add');
|
|
381
388
|
});
|
|
@@ -415,10 +422,9 @@ describe('runOrDiagnose', () => {
|
|
|
415
422
|
expect(result.content[0].text).not.toContain('Configured accounts');
|
|
416
423
|
});
|
|
417
424
|
|
|
418
|
-
it('returns plain error
|
|
425
|
+
it('returns plain error on a non-auth error (auth list is never consulted)', async () => {
|
|
419
426
|
vi.mocked(runner.run)
|
|
420
|
-
.mockRejectedValueOnce(new Error('Doc not found'))
|
|
421
|
-
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
427
|
+
.mockRejectedValueOnce(new Error('Doc not found'));
|
|
422
428
|
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
423
429
|
expect(result.content[0].text).toBe('Error: Doc not found');
|
|
424
430
|
expect(result.isError).toBe(true);
|
|
@@ -427,8 +433,7 @@ describe('runOrDiagnose', () => {
|
|
|
427
433
|
|
|
428
434
|
it('appends transient-retry hint on 429 error', async () => {
|
|
429
435
|
vi.mocked(runner.run)
|
|
430
|
-
.mockRejectedValueOnce(new Error('Request failed with status 429'))
|
|
431
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
436
|
+
.mockRejectedValueOnce(new Error('Request failed with status 429'));
|
|
432
437
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
433
438
|
expect(result.content[0].text).toContain('transient');
|
|
434
439
|
expect(result.content[0].text).toContain('Retry');
|
|
@@ -438,8 +443,7 @@ describe('runOrDiagnose', () => {
|
|
|
438
443
|
for (const status of [500, 502, 503, 504]) {
|
|
439
444
|
vi.clearAllMocks();
|
|
440
445
|
vi.mocked(runner.run)
|
|
441
|
-
.mockRejectedValueOnce(new Error(`Request failed with status ${status}`))
|
|
442
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
446
|
+
.mockRejectedValueOnce(new Error(`Request failed with status ${status}`));
|
|
443
447
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
444
448
|
expect(result.content[0].text, `status ${status}`).toContain('transient');
|
|
445
449
|
}
|
|
@@ -449,8 +453,7 @@ describe('runOrDiagnose', () => {
|
|
|
449
453
|
for (const msg of ['Quota exceeded', 'rateLimitExceeded', 'userRateLimitExceeded']) {
|
|
450
454
|
vi.clearAllMocks();
|
|
451
455
|
vi.mocked(runner.run)
|
|
452
|
-
.mockRejectedValueOnce(new Error(msg))
|
|
453
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
456
|
+
.mockRejectedValueOnce(new Error(msg));
|
|
454
457
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
455
458
|
expect(result.content[0].text, msg).toContain('transient');
|
|
456
459
|
}
|
|
@@ -458,16 +461,14 @@ describe('runOrDiagnose', () => {
|
|
|
458
461
|
|
|
459
462
|
it('appends transient-retry hint on DEADLINE_EXCEEDED error', async () => {
|
|
460
463
|
vi.mocked(runner.run)
|
|
461
|
-
.mockRejectedValueOnce(new Error('DEADLINE_EXCEEDED: context deadline exceeded'))
|
|
462
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
464
|
+
.mockRejectedValueOnce(new Error('DEADLINE_EXCEEDED: context deadline exceeded'));
|
|
463
465
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
464
466
|
expect(result.content[0].text).toContain('transient');
|
|
465
467
|
});
|
|
466
468
|
|
|
467
469
|
it('does not append transient hint on 404 error', async () => {
|
|
468
470
|
vi.mocked(runner.run)
|
|
469
|
-
.mockRejectedValueOnce(new Error('Request failed with status 404'))
|
|
470
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
471
|
+
.mockRejectedValueOnce(new Error('Request failed with status 404'));
|
|
471
472
|
const result = await runOrDiagnose(['sheets', 'get', pos('abc'), pos('A1')], {});
|
|
472
473
|
expect(result.content[0].text).not.toContain('transient');
|
|
473
474
|
});
|
|
@@ -481,10 +482,9 @@ describe('runOrDiagnose', () => {
|
|
|
481
482
|
expect(result.content[0].text).not.toContain('transient');
|
|
482
483
|
});
|
|
483
484
|
|
|
484
|
-
it('keeps transient hint
|
|
485
|
+
it('keeps the transient hint and names no accounts on a transient failure', async () => {
|
|
485
486
|
vi.mocked(runner.run)
|
|
486
|
-
.mockRejectedValueOnce(new Error('Request failed with status 503'))
|
|
487
|
-
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
487
|
+
.mockRejectedValueOnce(new Error('Request failed with status 503'));
|
|
488
488
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
489
489
|
expect(result.content[0].text).toContain('transient');
|
|
490
490
|
expect(result.content[0].text).not.toContain('Configured accounts');
|
|
@@ -492,8 +492,7 @@ describe('runOrDiagnose', () => {
|
|
|
492
492
|
|
|
493
493
|
it('appends grid-limit hint pointing at gog_sheets_insert', async () => {
|
|
494
494
|
vi.mocked(runner.run)
|
|
495
|
-
.mockRejectedValueOnce(new Error('Range (Sheet1!AP1:AW1) exceeds grid limits. Max rows: 1000, max columns: 41'))
|
|
496
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
495
|
+
.mockRejectedValueOnce(new Error('Range (Sheet1!AP1:AW1) exceeds grid limits. Max rows: 1000, max columns: 41'));
|
|
497
496
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('AP1')], {});
|
|
498
497
|
expect(result.content[0].text).toContain('exceeds grid limits');
|
|
499
498
|
expect(result.content[0].text).toContain('gog_sheets_insert');
|
|
@@ -501,16 +500,14 @@ describe('runOrDiagnose', () => {
|
|
|
501
500
|
|
|
502
501
|
it('does not append grid-limit hint on unrelated errors', async () => {
|
|
503
502
|
vi.mocked(runner.run)
|
|
504
|
-
.mockRejectedValueOnce(new Error('Spreadsheet not found'))
|
|
505
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
503
|
+
.mockRejectedValueOnce(new Error('Spreadsheet not found'));
|
|
506
504
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
507
505
|
expect(result.content[0].text).not.toContain('gog_sheets_insert');
|
|
508
506
|
});
|
|
509
507
|
|
|
510
|
-
it('keeps grid-limit hint
|
|
508
|
+
it('keeps the grid-limit hint and names no accounts on a grid failure', async () => {
|
|
511
509
|
vi.mocked(runner.run)
|
|
512
|
-
.mockRejectedValueOnce(new Error('exceeds grid limits. Max rows: 1000, max columns: 41'))
|
|
513
|
-
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
510
|
+
.mockRejectedValueOnce(new Error('exceeds grid limits. Max rows: 1000, max columns: 41'));
|
|
514
511
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
515
512
|
expect(result.content[0].text).toContain('gog_sheets_insert');
|
|
516
513
|
expect(result.content[0].text).not.toContain('Configured accounts');
|