gogcli-mcp 2.30.0 → 4.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.
Files changed (64) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/index.js +15345 -18417
  4. package/dist/lib.js +13720 -9481
  5. package/manifest.json +2 -2
  6. package/mint.yaml +37 -33
  7. package/package.json +5 -5
  8. package/server.json +2 -2
  9. package/src/attachments.ts +28 -34
  10. package/src/blob-upload.ts +165 -134
  11. package/src/blob-urls.ts +3 -5
  12. package/src/bootstrap-auth.ts +97 -0
  13. package/src/gmail-dispatch-guard.ts +106 -0
  14. package/src/gmail-results.ts +1 -1
  15. package/src/index.ts +3 -4
  16. package/src/lib.ts +24 -9
  17. package/src/pagination.ts +1 -1
  18. package/src/runner.ts +23 -175
  19. package/src/tools/api.ts +7 -7
  20. package/src/tools/appscript.ts +16 -16
  21. package/src/tools/auth.ts +16 -16
  22. package/src/tools/calendar.ts +13 -13
  23. package/src/tools/chat.ts +25 -25
  24. package/src/tools/classroom.ts +49 -49
  25. package/src/tools/contacts.ts +9 -9
  26. package/src/tools/docs.ts +13 -13
  27. package/src/tools/drive.ts +23 -25
  28. package/src/tools/gmail.ts +145 -32
  29. package/src/tools/sheets.ts +15 -15
  30. package/src/tools/slides.ts +13 -13
  31. package/src/tools/tasks.ts +13 -13
  32. package/src/tools/utils.ts +14 -61
  33. package/tests/attachments.test.ts +11 -14
  34. package/tests/blob-upload.test.ts +235 -160
  35. package/tests/bootstrap-auth.test.ts +245 -0
  36. package/tests/gmail-dispatch-guard.test.ts +132 -0
  37. package/tests/runner-file-args.test.ts +1 -13
  38. package/tests/runner.test.ts +8 -95
  39. package/tests/sdk-single-copy.test.ts +11 -37
  40. package/tests/tools/appscript.test.ts +1 -1
  41. package/tests/tools/auth-401-shapes.test.ts +2 -3
  42. package/tests/tools/auth.test.ts +5 -4
  43. package/tests/tools/chat.test.ts +1 -1
  44. package/tests/tools/drive.test.ts +11 -3
  45. package/tests/tools/gmail.test.ts +244 -14
  46. package/tests/tools/sheets.test.ts +1 -1
  47. package/tests/tools/utils.test.ts +1 -50
  48. package/tests/zod-single-copy.test.ts +8 -16
  49. package/tsconfig.json +1 -4
  50. package/vitest.config.ts +2 -14
  51. package/src/auth-log.ts +0 -205
  52. package/src/connector-auth.ts +0 -303
  53. package/src/connector-runtime.ts +0 -887
  54. package/src/google-probe.ts +0 -113
  55. package/src/google-token.ts +0 -391
  56. package/src/remote-runner.ts +0 -77
  57. package/src/worker.ts +0 -129
  58. package/tests/auth-log.test.ts +0 -530
  59. package/tests/connector-auth.test.ts +0 -559
  60. package/tests/connector-runtime.test.ts +0 -1644
  61. package/tests/google-probe.test.ts +0 -116
  62. package/tests/google-token.test.ts +0 -425
  63. package/tests/remote-runner.test.ts +0 -202
  64. package/tests/worker.test.ts +0 -167
@@ -38,7 +38,7 @@ describe('gog_chat_spaces_list', () => {
38
38
  // description a model reads — a source comment would not reach it. Every chat
39
39
  // tool carries the note, because any of them can be the first one called.
40
40
  it('warns in EVERY description that Chat is Workspace-only', async () => {
41
- const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
41
+ const { McpServer } = await import('@modelcontextprotocol/server');
42
42
  const server = new McpServer({ name: 'test', version: '0.0.0' });
43
43
  const configs = new Map<string, { description?: string }>();
44
44
  vi.spyOn(server, 'registerTool').mockImplementation((name, config) => {
@@ -468,14 +468,22 @@ describe('gog_drive_read_bytes', () => {
468
468
  expect(res.resource.uri).toBe('gogdrive://f2/file');
469
469
  });
470
470
 
471
- it('surfaces the connector-degradation error via diagnose', async () => {
471
+ it('surfaces a byte-fetch failure via diagnose', async () => {
472
472
  vi.mocked(runner.run)
473
473
  .mockResolvedValueOnce(JSON.stringify({ file: { name: 'a.pdf', mimeType: 'application/pdf' } }))
474
474
  .mockResolvedValueOnce('user@x.com'); // diagnose -> auth list
475
- vi.mocked(runner.runBinary).mockRejectedValueOnce(new Error('Raw byte retrieval is not available over the hosted connector'));
475
+ vi.mocked(runner.runBinary).mockRejectedValueOnce(new Error('gog exited with code 1'));
476
476
  const harness = await setupHandlers();
477
477
  const result = await harness.callTool('gog_drive_read_bytes', { fileId: 'f1' });
478
478
  expect(result.isError).toBe(true);
479
- expect(result.content[0].text).toContain('not available over the hosted connector');
479
+ expect(result.content[0].text).toContain('gog exited with code 1');
480
+ });
481
+
482
+ it('no longer claims the hosted transport cannot carry bytes', async () => {
483
+ const harness = await setupHandlers();
484
+ const { tools } = await harness.client.listTools();
485
+ const desc = tools.find((t) => t.name === 'gog_drive_read_bytes')!.description!;
486
+ expect(desc).not.toMatch(/text-only|hosted connector/i);
487
+ await harness.close();
480
488
  });
481
489
  });
@@ -3,10 +3,14 @@ import { registerGmailTools } from '../../src/tools/gmail.js';
3
3
  import * as runner from '../../src/runner.js';
4
4
  import { PAYLOAD_INLINE_MAX } from '../../src/tools/utils.js';
5
5
  import { createTestHarness } from '@chrischall/mcp-utils/test';
6
+ import type { ElicitRequest, ElicitResult } from '@modelcontextprotocol/server';
6
7
 
7
8
  vi.mock('../../src/runner.js');
8
9
 
9
- const setupHandlers = () => createTestHarness(registerGmailTools);
10
+ const setupHandlers = (
11
+ elicitation: (request: ElicitRequest) => ElicitResult | Promise<ElicitResult> =
12
+ async () => ({ action: 'accept', content: { confirmed: true } }),
13
+ ) => createTestHarness(registerGmailTools, { elicitation });
10
14
 
11
15
  beforeEach(() => vi.clearAllMocks());
12
16
 
@@ -300,8 +304,8 @@ describe('gog_gmail_send', () => {
300
304
 
301
305
  // ==========================================================================
302
306
  // INLINE ATTACHMENT BYTES — for callers with no filesystem in common with gog
303
- // (the hosted connector, any GOG_RUNNER_URL backend). The bytes ride with the
304
- // call and the executor materializes them beside gog.
307
+ // (a hosted deployment such as mcp-host). The bytes ride with the call and
308
+ // the runner materializes them beside gog.
305
309
  // ==========================================================================
306
310
  it('turns attachInline bytes into repeatable --attach file args', async () => {
307
311
  vi.mocked(runner.run).mockResolvedValue('{}');
@@ -425,6 +429,88 @@ describe('gog_gmail_send', () => {
425
429
  const result = await harness.callTool('gog_gmail_send', { to: 'x', subject: 'y', body: 'z' });
426
430
  expect(result.content[0].text).toBe('Error: Send failed');
427
431
  });
432
+
433
+ // ==========================================================================
434
+ // THE CONFIRMATION GATE. Unlike reply/reply-all, send's recipients are
435
+ // explicit params, so its preview needs no extra gog call at all.
436
+ // ==========================================================================
437
+ describe('confirmation gate', () => {
438
+ it('elicits a recipient preview and sends only after the user accepts it', async () => {
439
+ vi.mocked(runner.run).mockResolvedValue('{}');
440
+ let request: ElicitRequest | undefined;
441
+ const harness = await setupHandlers(async (value) => {
442
+ request = value;
443
+ return { action: 'accept', content: { confirmed: true } };
444
+ });
445
+ const result = await harness.callTool('gog_gmail_send', {
446
+ to: 'bob@example.com', cc: 'carol@example.com', subject: 'Hi', body: 'Hello',
447
+ replyToMessageId: 'm1', quote: true,
448
+ });
449
+ expect(request?.params.message).toContain('bob@example.com');
450
+ expect(request?.params.message).toContain('carol@example.com');
451
+ expect(request?.params.message).toContain('"threaded": true');
452
+ expect(request?.params.requestedSchema).toEqual(expect.objectContaining({ type: 'object' }));
453
+ expect(result.isError).not.toBe(true);
454
+ expect(runner.run).toHaveBeenCalledTimes(1);
455
+ });
456
+
457
+ it('sends nothing when the user declines the elicitation', async () => {
458
+ const harness = await setupHandlers(async () => ({ action: 'decline' }));
459
+ const result = await harness.callTool('gog_gmail_send', {
460
+ to: 'bob@example.com', subject: 'Hi', body: 'Hello',
461
+ });
462
+ expect(runner.run).not.toHaveBeenCalled();
463
+ expect(JSON.parse(result.content[0].text as string)).toEqual(expect.objectContaining({
464
+ confirmed: false,
465
+ cancelled: true,
466
+ action: 'gmail.send',
467
+ }));
468
+ });
469
+
470
+ it('sends nothing when the user accepts but leaves confirmation false', async () => {
471
+ const harness = await setupHandlers(async () => ({ action: 'accept', content: { confirmed: false } }));
472
+ const result = await harness.callTool('gog_gmail_send', {
473
+ to: 'bob@example.com', subject: 'Hi', body: 'Hello',
474
+ });
475
+ expect(runner.run).not.toHaveBeenCalled();
476
+ expect(JSON.parse(result.content[0].text as string).cancelled).toBe(true);
477
+ });
478
+
479
+ it('logs a gmail_dispatch event with the recipients after an accepted send', async () => {
480
+ vi.mocked(runner.run).mockResolvedValue('{}');
481
+ const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
482
+ const harness = await setupHandlers();
483
+ await harness.callTool('gog_gmail_send', {
484
+ to: 'bob@example.com', subject: 'Hi', body: 'Hello',
485
+ });
486
+ const event = JSON.parse((writeSpy.mock.calls.at(-1)?.[0] as string).trim());
487
+ expect(event.event).toBe('gmail_dispatch');
488
+ expect(event.tool).toBe('gog_gmail_send');
489
+ expect(event.recipientCount).toBe(1);
490
+ writeSpy.mockRestore();
491
+ });
492
+
493
+ it('does not log when the accepted send fails', async () => {
494
+ vi.mocked(runner.run).mockRejectedValue(new Error('Send failed'));
495
+ const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
496
+ const harness = await setupHandlers();
497
+ await harness.callTool('gog_gmail_send', {
498
+ to: 'bob@example.com', subject: 'Hi', body: 'Hello',
499
+ });
500
+ expect(writeSpy).not.toHaveBeenCalled();
501
+ writeSpy.mockRestore();
502
+ });
503
+
504
+ it('still validates attachments before either preview or send', async () => {
505
+ const harness = await setupHandlers();
506
+ const res = await harness.callTool('gog_gmail_send', {
507
+ to: 'bob@example.com', subject: 's', body: 'b',
508
+ attachInline: [{ filename: 'a.png', contentBase64: 'not!valid!base64!' }],
509
+ });
510
+ expect(res.isError).toBe(true);
511
+ expect(runner.run).not.toHaveBeenCalled();
512
+ });
513
+ });
428
514
  });
429
515
 
430
516
  describe('gog_gmail_run', () => {
@@ -630,7 +716,9 @@ describe('gog_gmail_reply — full flag set', () => {
630
716
  it('omits --no-quote and --signature when false', async () => {
631
717
  vi.mocked(runner.run).mockResolvedValue('{}');
632
718
  const harness = await setupHandlers();
633
- await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Hi', noQuote: false, signature: false });
719
+ await harness.callTool('gog_gmail_reply', {
720
+ messageId: 'm1', body: 'Hi', noQuote: false, signature: false,
721
+ });
634
722
  expect(runner.run).toHaveBeenCalledWith(
635
723
  ['gmail', 'reply', 'm1', '--body=Hi', '--auto-from-addressed-alias=false'],
636
724
  { account: undefined },
@@ -672,6 +760,150 @@ describe('gog_gmail_reply_all — full flag set', () => {
672
760
  );
673
761
  });
674
762
  });
763
+
764
+ // ============================================================================
765
+ // THE CONFIRMATION GATE. The first protocol round returns input_required and
766
+ // the harness routes its embedded elicitation through the handler supplied to
767
+ // setupHandlers. Declining lets these tests inspect the prompt while proving
768
+ // the send subcommand was never reached.
769
+ // ============================================================================
770
+ describe.each([
771
+ ['gog_gmail_reply', 'reply' as const],
772
+ ['gog_gmail_reply_all', 'reply-all' as const],
773
+ ])('%s — confirmation gate', (tool, subcommand) => {
774
+ const METADATA = JSON.stringify({
775
+ headers: { from: 'Alice <alice@example.com>', to: 'me@example.com', cc: 'Carol <carol@law-firm.example>', subject: 'Contract terms' },
776
+ });
777
+
778
+ const promptedDetails = async (args: Record<string, unknown>, metadata = METADATA) => {
779
+ vi.mocked(runner.run).mockResolvedValue(METADATA);
780
+ if (metadata !== METADATA) vi.mocked(runner.run).mockResolvedValue(metadata);
781
+ let request: ElicitRequest | undefined;
782
+ const harness = await setupHandlers(async (value) => {
783
+ request = value;
784
+ return { action: 'decline' };
785
+ });
786
+ const result = await harness.callTool(tool, { messageId: 'm1', ...args });
787
+ return {
788
+ result,
789
+ request,
790
+ details: (JSON.parse(request?.params.message.split('\n').slice(1).join('\n') ?? '{}') as {
791
+ details?: Record<string, unknown>;
792
+ }).details ?? {},
793
+ };
794
+ };
795
+
796
+ it('elicits the resolved recipients and sends nothing when the user declines', async () => {
797
+ const { result, request, details } = await promptedDetails({ body: 'Sounds good' });
798
+ expect(runner.run).toHaveBeenCalledTimes(2);
799
+ expect(runner.run).toHaveBeenCalledWith(['gmail', 'get', 'm1', '--format=metadata'], { account: undefined });
800
+ expect(request?.params.requestedSchema).toEqual(expect.objectContaining({ type: 'object' }));
801
+ expect(JSON.parse(result.content[0].text as string)).toEqual(expect.objectContaining({
802
+ confirmed: false,
803
+ cancelled: true,
804
+ action: `gmail.${subcommand}`,
805
+ }));
806
+ if (subcommand === 'reply') {
807
+ expect(details.recipients).toEqual(['alice@example.com']);
808
+ } else {
809
+ expect(details.recipients).toEqual(['alice@example.com', 'me@example.com', 'carol@law-firm.example']);
810
+ }
811
+ });
812
+
813
+ it('sends nothing when the user accepts but leaves confirmation false', async () => {
814
+ vi.mocked(runner.run).mockResolvedValue(METADATA);
815
+ const harness = await setupHandlers(async () => ({ action: 'accept', content: { confirmed: false } }));
816
+ const result = await harness.callTool(tool, { messageId: 'm1', body: 'x' });
817
+ expect(runner.run).toHaveBeenCalledTimes(2);
818
+ expect(JSON.parse(result.content[0].text as string).cancelled).toBe(true);
819
+ });
820
+
821
+ it('falls back to "Re: <original subject>" in the prompt when no subject override is given', async () => {
822
+ const { details } = await promptedDetails({ body: 'x' });
823
+ expect(details.subject).toBe('Re: Contract terms');
824
+ });
825
+
826
+ it('prefers an explicit subject override in the prompt over the inherited one', async () => {
827
+ const { details } = await promptedDetails({ body: 'x', subject: 'Custom subject' });
828
+ expect(details.subject).toBe('Custom subject');
829
+ });
830
+
831
+ it('actually sends and logs the resolved recipients after the user accepts', async () => {
832
+ vi.mocked(runner.run)
833
+ .mockResolvedValueOnce(METADATA)
834
+ .mockResolvedValueOnce(METADATA)
835
+ .mockResolvedValueOnce('{"id":"sent1"}');
836
+ const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
837
+ const harness = await setupHandlers();
838
+ const result = await harness.callTool(tool, { messageId: 'm1', body: 'Sounds good' });
839
+ expect(runner.run).toHaveBeenCalledTimes(3);
840
+ expect(runner.run).toHaveBeenNthCalledWith(1, ['gmail', 'get', 'm1', '--format=metadata'], { account: undefined });
841
+ expect(runner.run).toHaveBeenNthCalledWith(
842
+ 3,
843
+ ['gmail', subcommand, 'm1', '--body=Sounds good', '--auto-from-addressed-alias=false'],
844
+ { account: undefined },
845
+ );
846
+ expect(result.content[0].text).toBe('{"id":"sent1"}');
847
+ const event = JSON.parse((writeSpy.mock.calls.at(-1)?.[0] as string).trim());
848
+ expect(event.event).toBe('gmail_dispatch');
849
+ expect(event.tool).toBe(tool);
850
+ expect(event.recipientCount).toBeGreaterThan(0);
851
+ writeSpy.mockRestore();
852
+ });
853
+
854
+ it('does not log when the accepted send itself fails', async () => {
855
+ vi.mocked(runner.run)
856
+ .mockResolvedValueOnce(METADATA)
857
+ .mockResolvedValueOnce(METADATA)
858
+ .mockRejectedValueOnce(new Error('quota exceeded'));
859
+ const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
860
+ const harness = await setupHandlers();
861
+ const result = await harness.callTool(tool, { messageId: 'm1', body: 'x' });
862
+ expect(result.isError).toBe(true);
863
+ expect(writeSpy).not.toHaveBeenCalled();
864
+ writeSpy.mockRestore();
865
+ });
866
+
867
+ it('surfaces a bad messageId as a diagnosed error, without ever building a preview', async () => {
868
+ vi.mocked(runner.run).mockRejectedValue(new Error('message not found'));
869
+ const harness = await setupHandlers();
870
+ const result = await harness.callTool(tool, { messageId: 'bad', body: 'x' });
871
+ expect(result.isError).toBe(true);
872
+ expect(result.content[0].text).toContain('message not found');
873
+ });
874
+
875
+ it('measures bodyLength from bodyHtml when no plain body is given', async () => {
876
+ const { details } = await promptedDetails({ bodyHtml: '<p>Hi</p>' });
877
+ expect(details.bodyLength).toBe('<p>Hi</p>'.length);
878
+ });
879
+
880
+ it('measures bodyLength as zero when neither body nor bodyHtml is given', async () => {
881
+ const { details } = await promptedDetails({});
882
+ expect(details.bodyLength).toBe(0);
883
+ });
884
+
885
+ it('ignores a non-string header value in the metadata response', async () => {
886
+ const { details } = await promptedDetails({ body: 'x' }, JSON.stringify({
887
+ headers: { from: 'alice@example.com', subject: 42 },
888
+ }));
889
+ expect(details.recipients).toEqual(['alice@example.com']);
890
+ expect(details.subject).toBeUndefined();
891
+ });
892
+
893
+ it('treats an unparseable metadata response as empty headers rather than crashing', async () => {
894
+ const { details } = await promptedDetails({ body: 'x' }, 'not json at all');
895
+ expect(details.recipients).toEqual([]);
896
+ });
897
+
898
+ it('applies to/cc/bcc adds and remove drops on top of the inherited recipients in the prompt', async () => {
899
+ const { details } = await promptedDetails({
900
+ body: 'x', to: ['dave@example.com'], remove: ['carol@law-firm.example'],
901
+ });
902
+ expect(details.recipients).not.toContain('carol@law-firm.example');
903
+ expect(details.recipients).toContain('dave@example.com');
904
+ });
905
+ });
906
+
675
907
  describe('gog_gmail_reply body-vs-file conflicts', () => {
676
908
  it('rejects bodyHtml plus bodyHtmlFile before gog runs', async () => {
677
909
  vi.mocked(runner.run).mockResolvedValue('{}');
@@ -699,7 +931,9 @@ describe('gog_gmail_reply body-vs-file conflicts', () => {
699
931
  it('passes bodyHtmlFile alone through as --body-html-file', async () => {
700
932
  vi.mocked(runner.run).mockResolvedValue('{}');
701
933
  const harness = await setupHandlers();
702
- await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Hi', bodyHtmlFile: '/tmp/b.html' });
934
+ await harness.callTool('gog_gmail_reply', {
935
+ messageId: 'm1', body: 'Hi', bodyHtmlFile: '/tmp/b.html',
936
+ });
703
937
  expect(runner.run).toHaveBeenCalledWith(
704
938
  ['gmail', 'reply', 'm1', '--body=Hi', '--body-html-file=/tmp/b.html', '--auto-from-addressed-alias=false'],
705
939
  { account: undefined },
@@ -729,17 +963,13 @@ describe('gog_gmail_reply body-vs-file conflicts', () => {
729
963
  // timeout. No file param may advertise it as usable.
730
964
  describe('reply file params never advertise stdin as usable', () => {
731
965
  it.each(['gog_gmail_reply', 'gog_gmail_reply_all'])('%s.bodyHtmlFile warns that stdin hangs', async (tool) => {
732
- const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
733
- const server = new McpServer({ name: 'test', version: '0.0.0' });
734
- const configs = new Map<string, Record<string, { description?: string }>>();
735
- vi.spyOn(server, 'registerTool').mockImplementation((name, config) => {
736
- configs.set(name, (config as { inputSchema: Record<string, { description?: string }> }).inputSchema);
737
- return undefined as never;
738
- });
739
- registerGmailTools(server);
740
- const desc = configs.get(tool)?.bodyHtmlFile?.description ?? '';
966
+ const harness = await setupHandlers();
967
+ const listed = (await harness.client.listTools()).tools.find((candidate) => candidate.name === tool);
968
+ const properties = listed?.inputSchema.properties as Record<string, { description?: string }> | undefined;
969
+ const desc = properties?.bodyHtmlFile?.description ?? '';
741
970
  expect(desc).not.toBe('');
742
971
  expect(desc).not.toMatch(/(?:or|use)\s+"?-"?\s+(?:for|to read)/i);
743
972
  expect(desc).toMatch(/stdin/i);
973
+ expect(properties).not.toHaveProperty('confirmed');
744
974
  });
745
975
  });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { McpServer } from '@modelcontextprotocol/server';
3
3
  import { registerSheetsTools } from '../../src/tools/sheets.js';
4
4
  import * as runner from '../../src/runner.js';
5
5
  import { createTestHarness } from '@chrischall/mcp-utils/test';
@@ -1,13 +1,8 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import * as runner from '../../src/runner.js';
3
3
  import { runOrDiagnose, pushPaginationFlags, formatAccountList, formatAuthHealth } from '../../src/tools/utils.js';
4
- import { RunnerTransportError } from '../../src/connector-runtime.js';
5
4
 
6
- // PARTIAL mock: only `run` is stubbed. A full automock would also replace
7
- // `RunnerTransportError` and `isRunnerTransportError`, and diagnose()'s whole
8
- // point is that it classifies the REAL class structurally — an automocked
9
- // brand check answers undefined for everything and the tests below would pass
10
- // while proving nothing.
5
+ // PARTIAL mock: only `run` is stubbed; everything else stays real.
11
6
  vi.mock('../../src/runner.js', async (importOriginal) => ({
12
7
  ...(await importOriginal<typeof runner>()),
13
8
  run: vi.fn(),
@@ -605,47 +600,3 @@ describe('formatAuthHealth', () => {
605
600
  expect(formatAuthHealth('{"foo":1}', NOW)).toBe('{"foo":1}');
606
601
  });
607
602
  });
608
-
609
- // The connector's own transport failing is NOT the Google credential failing.
610
- // diagnose() must recognise that structurally, from the error's type, because
611
- // the runner's prose ("unauthorized") is indistinguishable from Google's.
612
- describe('diagnose: runner-authored transport failures', () => {
613
- it('blames the runner key, not the Google account, for a transport-auth failure', async () => {
614
- vi.mocked(runner.run)
615
- .mockRejectedValueOnce(
616
- new RunnerTransportError('gog-runner rejected the bearer token; GOG_RUNNER_KEY vs RUNNER_KEY', 'transport-auth', 401),
617
- )
618
- .mockResolvedValueOnce('user@gmail.com');
619
- const result = await runOrDiagnose(['sheets', 'get', 'A1'], {});
620
- const text = result.content[0].text as string;
621
- expect(text).not.toContain('gog_auth_add');
622
- expect(text).not.toMatch(/re-authorize the account/i);
623
- expect(text).toContain('GOG_RUNNER_KEY');
624
- expect(text).toContain('RUNNER_KEY');
625
- });
626
-
627
- it('lets the TYPE beat the prose: runner words that read like Google words get no re-auth hint', async () => {
628
- // The regression this whole change exists to prevent. A runner-authored
629
- // failure whose text happens to contain every Google auth signal must still
630
- // not produce Google auth advice — the request never reached Google.
631
- vi.mocked(runner.run)
632
- .mockRejectedValueOnce(
633
- new RunnerTransportError('unauthorized: 401 invalid_grant, token has been expired or revoked', 'transport-request', 400),
634
- )
635
- .mockResolvedValueOnce('user@gmail.com');
636
- const result = await runOrDiagnose(['sheets', 'get', 'A1'], {});
637
- const text = result.content[0].text as string;
638
- expect(text).not.toContain('gog_auth_add');
639
- expect(text).not.toContain('often transient');
640
- });
641
-
642
- it('advises a retry for a retryable transport failure', async () => {
643
- vi.mocked(runner.run)
644
- .mockRejectedValueOnce(new RunnerTransportError('gog-runner is restarting; retry this call.', 'transport-retryable', 503))
645
- .mockResolvedValueOnce('user@gmail.com');
646
- const result = await runOrDiagnose(['sheets', 'get', 'A1'], {});
647
- const text = result.content[0].text as string;
648
- expect(text).toContain('often transient');
649
- expect(text).not.toContain('gog_auth_add');
650
- });
651
- });
@@ -6,12 +6,12 @@ import { fileURLToPath } from 'node:url';
6
6
  // The zod counterpart of sdk-single-copy.test.ts, guarding the invariant that
7
7
  // broke dependabot #333: the whole monorepo must resolve ONE copy of zod.
8
8
  //
9
- // Same shape of failure, a different package. `@cloudflare/vitest-pool-workers`
10
- // (a root devDependency) declares zod as an **exact** pin, so it takes the
11
- // hoisted root slot that `@chrischall/mcp-utils` and the MCP SDK resolve their
12
- // zod peer from. The moment our workspaces ask for a newer zod than that pin,
13
- // each nests its own copy — and because a `ZodType` carries brand-bearing
14
- // internals, TypeScript compares the two NOMINALLY: every schema our tools hand
9
+ // Same shape of failure, a different package. Any dependency that pins zod
10
+ // can take the hoisted root slot that `@chrischall/mcp-utils` and the MCP SDK
11
+ // resolve their zod peer from (#333: a since-removed devDependency's exact pin).
12
+ // The moment our workspaces ask for a newer zod than that pin, each nests its
13
+ // own copy — and because a `ZodType` carries brand-bearing internals,
14
+ // TypeScript compares the two NOMINALLY: every schema our tools hand
15
15
  // `registerTool` fails with `TS2322: Type 'ZodString' is not assignable to type
16
16
  // 'AnySchema'`, with no API change and nothing to fix in the source. #333 split
17
17
  // the tree exactly that way and produced 11,024 type errors from a bump of one
@@ -38,18 +38,10 @@ describe('zod is installed exactly once', () => {
38
38
  expect(resolveFrom('@chrischall/mcp-utils')).toBe(realpathSync(here.resolve('zod')));
39
39
  });
40
40
 
41
- it('resolves to the same file for the MCP SDK, which types every tool schema', () => {
41
+ it('resolves to the same file for the MCP server SDK, which types every tool schema', () => {
42
42
  // `registerTool` accepts the raw shape and infers the handler's argument
43
43
  // types from it; a second copy makes every one of those schemas foreign.
44
- expect(resolveFrom('@modelcontextprotocol/sdk/server/mcp.js')).toBe(
45
- realpathSync(here.resolve('zod')),
46
- );
47
- });
48
-
49
- it('resolves to the same file for @cloudflare/vitest-pool-workers, which exact-pins zod', () => {
50
- // The exact pin here is what captured the root hoist slot in #333, and it
51
- // is why the root `overrides` block carries a zod entry.
52
- expect(resolveFrom('@cloudflare/vitest-pool-workers')).toBe(
44
+ expect(resolveFrom('@modelcontextprotocol/server')).toBe(
53
45
  realpathSync(here.resolve('zod')),
54
46
  );
55
47
  });
package/tsconfig.json CHANGED
@@ -5,8 +5,5 @@
5
5
  "rootDir": "./src"
6
6
  },
7
7
  "include": ["src/**/*"],
8
- // src/worker.ts is the Cloudflare-Worker entry: it imports
9
- // @chrischall/mcp-connector (which pulls the `agents` runtime + Cloudflare
10
- // types) and is compiled by wrangler/esbuild, not by this stdio tsc build.
11
- "exclude": ["node_modules", "dist", "src/worker.ts"]
8
+ "exclude": ["node_modules", "dist"]
12
9
  }
package/vitest.config.ts CHANGED
@@ -1,11 +1,7 @@
1
- import { configDefaults, defineConfig } from 'vitest/config';
1
+ import { defineConfig } from 'vitest/config';
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
5
- // `tests/worker.test.ts` only runs under the Workers runtime pool
6
- // (root `vitest.workers.config.mts` / `npm run worker:test`), which provides
7
- // the virtual `cloudflare:test` module it imports. The node pool must skip it.
8
- exclude: [...configDefaults.exclude, 'tests/worker.test.ts'],
9
5
  // Neutralize the gog env vars for the whole suite.
10
6
  //
11
7
  // THE BUG THIS FIXES: the runner tests assert the exact argv `run()` builds,
@@ -28,15 +24,7 @@ export default defineConfig({
28
24
  coverage: {
29
25
  provider: 'v8',
30
26
  include: ['src/**/*.ts'],
31
- exclude: [
32
- 'src/index.ts',
33
- // src/worker.ts is the only Worker-path file that can't load under the
34
- // node pool (it imports the `@chrischall/mcp-connector`/`agents` runtime);
35
- // it's exercised by the Workers pool suite (`npm run worker:test`). Its
36
- // testable helpers live in src/connector-runtime.ts, and src/connector-auth.ts
37
- // is node-loadable — both are unit-tested here and stay in the 100% gate.
38
- 'src/worker.ts',
39
- ],
27
+ exclude: ['src/index.ts'],
40
28
  thresholds: {
41
29
  lines: 100,
42
30
  functions: 100,