@google/gemini-cli-core 0.21.0-nightly.20251203.533a3fb31 → 0.21.0-nightly.20251205.f4f2bcbd9

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 (127) hide show
  1. package/dist/google-gemini-cli-core-0.21.0-nightly.20251202.2d935b379.tgz +0 -0
  2. package/dist/src/code_assist/oauth2.d.ts +2 -0
  3. package/dist/src/code_assist/oauth2.js +28 -12
  4. package/dist/src/code_assist/oauth2.js.map +1 -1
  5. package/dist/src/code_assist/oauth2.test.js +40 -2
  6. package/dist/src/code_assist/oauth2.test.js.map +1 -1
  7. package/dist/src/commands/restore.d.ts +19 -0
  8. package/dist/src/commands/restore.js +45 -0
  9. package/dist/src/commands/restore.js.map +1 -0
  10. package/dist/src/commands/restore.test.d.ts +6 -0
  11. package/dist/src/commands/restore.test.js +136 -0
  12. package/dist/src/commands/restore.test.js.map +1 -0
  13. package/dist/src/commands/types.d.ts +41 -0
  14. package/dist/src/commands/types.js +7 -0
  15. package/dist/src/commands/types.js.map +1 -0
  16. package/dist/src/config/config.d.ts +17 -2
  17. package/dist/src/config/config.js +28 -0
  18. package/dist/src/config/config.js.map +1 -1
  19. package/dist/src/core/client.js +2 -1
  20. package/dist/src/core/client.js.map +1 -1
  21. package/dist/src/core/client.test.js +23 -0
  22. package/dist/src/core/client.test.js.map +1 -1
  23. package/dist/src/core/geminiChat.test.js +4 -0
  24. package/dist/src/core/geminiChat.test.js.map +1 -1
  25. package/dist/src/core/sessionHookTriggers.d.ts +28 -0
  26. package/dist/src/core/sessionHookTriggers.js +68 -0
  27. package/dist/src/core/sessionHookTriggers.js.map +1 -0
  28. package/dist/src/generated/git-commit.d.ts +2 -2
  29. package/dist/src/generated/git-commit.js +2 -2
  30. package/dist/src/hooks/hookEventHandler.js +51 -0
  31. package/dist/src/hooks/hookEventHandler.js.map +1 -1
  32. package/dist/src/hooks/hookRegistry.js +8 -1
  33. package/dist/src/hooks/hookRegistry.js.map +1 -1
  34. package/dist/src/hooks/hookRegistry.test.js +1 -0
  35. package/dist/src/hooks/hookRegistry.test.js.map +1 -1
  36. package/dist/src/hooks/hookRunner.js +12 -2
  37. package/dist/src/hooks/hookRunner.js.map +1 -1
  38. package/dist/src/hooks/hookSystem.test.js +124 -0
  39. package/dist/src/hooks/hookSystem.test.js.map +1 -1
  40. package/dist/src/hooks/index.d.ts +3 -1
  41. package/dist/src/hooks/index.js +3 -0
  42. package/dist/src/hooks/index.js.map +1 -1
  43. package/dist/src/hooks/types.d.ts +1 -2
  44. package/dist/src/hooks/types.js +0 -1
  45. package/dist/src/hooks/types.js.map +1 -1
  46. package/dist/src/index.d.ts +2 -0
  47. package/dist/src/index.js +2 -0
  48. package/dist/src/index.js.map +1 -1
  49. package/dist/src/output/json-formatter.d.ts +2 -2
  50. package/dist/src/output/json-formatter.js +6 -3
  51. package/dist/src/output/json-formatter.js.map +1 -1
  52. package/dist/src/output/json-formatter.test.js +35 -9
  53. package/dist/src/output/json-formatter.test.js.map +1 -1
  54. package/dist/src/output/types.d.ts +1 -0
  55. package/dist/src/output/types.js.map +1 -1
  56. package/dist/src/services/chatCompressionService.js +12 -0
  57. package/dist/src/services/chatCompressionService.js.map +1 -1
  58. package/dist/src/services/chatCompressionService.test.js +2 -0
  59. package/dist/src/services/chatCompressionService.test.js.map +1 -1
  60. package/dist/src/services/shellExecutionService.js +5 -18
  61. package/dist/src/services/shellExecutionService.js.map +1 -1
  62. package/dist/src/services/shellExecutionService.test.js +29 -2
  63. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  64. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +1 -0
  65. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +20 -0
  66. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  67. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +49 -0
  68. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  69. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +1 -0
  70. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +3 -1
  71. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  72. package/dist/src/telemetry/config.js +2 -0
  73. package/dist/src/telemetry/config.js.map +1 -1
  74. package/dist/src/telemetry/config.test.js +25 -0
  75. package/dist/src/telemetry/config.test.js.map +1 -1
  76. package/dist/src/telemetry/gcp-exporters.d.ts +4 -3
  77. package/dist/src/telemetry/gcp-exporters.js +7 -4
  78. package/dist/src/telemetry/gcp-exporters.js.map +1 -1
  79. package/dist/src/telemetry/index.d.ts +1 -1
  80. package/dist/src/telemetry/index.js +1 -1
  81. package/dist/src/telemetry/index.js.map +1 -1
  82. package/dist/src/telemetry/loggers.js +335 -335
  83. package/dist/src/telemetry/loggers.js.map +1 -1
  84. package/dist/src/telemetry/loggers.test.js +15 -0
  85. package/dist/src/telemetry/loggers.test.js.map +1 -1
  86. package/dist/src/telemetry/sdk.d.ts +9 -2
  87. package/dist/src/telemetry/sdk.js +126 -16
  88. package/dist/src/telemetry/sdk.js.map +1 -1
  89. package/dist/src/telemetry/sdk.test.js +111 -28
  90. package/dist/src/telemetry/sdk.test.js.map +1 -1
  91. package/dist/src/telemetry/startupProfiler.test.js +4 -0
  92. package/dist/src/telemetry/startupProfiler.test.js.map +1 -1
  93. package/dist/src/telemetry/telemetry.test.js +10 -3
  94. package/dist/src/telemetry/telemetry.test.js.map +1 -1
  95. package/dist/src/tools/mcp-client-manager.js +15 -4
  96. package/dist/src/tools/mcp-client-manager.js.map +1 -1
  97. package/dist/src/tools/mcp-client.d.ts +17 -2
  98. package/dist/src/tools/mcp-client.js +319 -166
  99. package/dist/src/tools/mcp-client.js.map +1 -1
  100. package/dist/src/tools/mcp-client.test.js +466 -26
  101. package/dist/src/tools/mcp-client.test.js.map +1 -1
  102. package/dist/src/tools/modifiable-tool.test.js +22 -13
  103. package/dist/src/tools/modifiable-tool.test.js.map +1 -1
  104. package/dist/src/utils/debugLogger.d.ts +3 -0
  105. package/dist/src/utils/debugLogger.js +27 -0
  106. package/dist/src/utils/debugLogger.js.map +1 -1
  107. package/dist/src/utils/editCorrector.test.js +4 -0
  108. package/dist/src/utils/editCorrector.test.js.map +1 -1
  109. package/dist/src/utils/editor.d.ts +9 -1
  110. package/dist/src/utils/editor.js +23 -14
  111. package/dist/src/utils/editor.js.map +1 -1
  112. package/dist/src/utils/errors.d.ts +8 -0
  113. package/dist/src/utils/errors.js +32 -0
  114. package/dist/src/utils/errors.js.map +1 -1
  115. package/dist/src/utils/errors.test.d.ts +6 -0
  116. package/dist/src/utils/errors.test.js +36 -0
  117. package/dist/src/utils/errors.test.js.map +1 -0
  118. package/dist/src/utils/nextSpeakerChecker.test.js +4 -0
  119. package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -1
  120. package/dist/src/utils/retry.js +38 -5
  121. package/dist/src/utils/retry.js.map +1 -1
  122. package/dist/src/utils/retry.test.js +35 -4
  123. package/dist/src/utils/retry.test.js.map +1 -1
  124. package/dist/src/utils/terminalSerializer.test.js +17 -0
  125. package/dist/src/utils/terminalSerializer.test.js.map +1 -1
  126. package/dist/tsconfig.tsbuildinfo +1 -1
  127. package/package.json +1 -1
@@ -6,13 +6,14 @@
6
6
  import * as ClientLib from '@modelcontextprotocol/sdk/client/index.js';
7
7
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
8
8
  import * as SdkClientStdioLib from '@modelcontextprotocol/sdk/client/stdio.js';
9
- import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
9
+ import { StreamableHTTPClientTransport, StreamableHTTPError, } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
10
10
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
11
11
  import { AuthProviderType } from '../config/config.js';
12
12
  import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
13
13
  import { MCPOAuthProvider } from '../mcp/oauth-provider.js';
14
14
  import { MCPOAuthTokenStorage } from '../mcp/oauth-token-storage.js';
15
15
  import { OAuthUtils } from '../mcp/oauth-utils.js';
16
+ import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
16
17
  import { WorkspaceContext } from '../utils/workspaceContext.js';
17
18
  import { connectToMcpServer, createTransport, hasNetworkTransport, isEnabled, McpClient, populateMcpServerCommand, } from './mcp-client.js';
18
19
  import * as fs from 'node:fs';
@@ -80,10 +81,10 @@ describe('mcp-client', () => {
80
81
  };
81
82
  const client = new McpClient('test-server', {
82
83
  command: 'test-command',
83
- }, mockedToolRegistry, {}, workspaceContext, false);
84
+ }, mockedToolRegistry, {}, workspaceContext, {}, false);
84
85
  await client.connect();
85
86
  await client.discover({});
86
- expect(mockedClient.listTools).toHaveBeenCalledWith({});
87
+ expect(mockedClient.listTools).toHaveBeenCalledWith({}, { timeout: 600000 });
87
88
  });
88
89
  it('should not skip tools even if a parameter is missing a type', async () => {
89
90
  const consoleWarnSpy = vi
@@ -133,7 +134,7 @@ describe('mcp-client', () => {
133
134
  };
134
135
  const client = new McpClient('test-server', {
135
136
  command: 'test-command',
136
- }, mockedToolRegistry, {}, workspaceContext, false);
137
+ }, mockedToolRegistry, {}, workspaceContext, {}, false);
137
138
  await client.connect();
138
139
  await client.discover({});
139
140
  expect(mockedToolRegistry.registerTool).toHaveBeenCalledTimes(2);
@@ -161,7 +162,7 @@ describe('mcp-client', () => {
161
162
  };
162
163
  const client = new McpClient('test-server', {
163
164
  command: 'test-command',
164
- }, mockedToolRegistry, {}, workspaceContext, false);
165
+ }, mockedToolRegistry, {}, workspaceContext, {}, false);
165
166
  await client.connect();
166
167
  await expect(client.discover({})).rejects.toThrow('No prompts or tools found on the server.');
167
168
  expect(coreEvents.emitFeedback).toHaveBeenCalledWith('error', `Error discovering prompts from test-server: Test error`, expect.any(Error));
@@ -187,7 +188,7 @@ describe('mcp-client', () => {
187
188
  };
188
189
  const client = new McpClient('test-server', {
189
190
  command: 'test-command',
190
- }, mockedToolRegistry, {}, workspaceContext, false);
191
+ }, mockedToolRegistry, {}, workspaceContext, {}, false);
191
192
  await client.connect();
192
193
  await expect(client.discover({})).rejects.toThrow('No prompts or tools found on the server.');
193
194
  });
@@ -221,7 +222,7 @@ describe('mcp-client', () => {
221
222
  };
222
223
  const client = new McpClient('test-server', {
223
224
  command: 'test-command',
224
- }, mockedToolRegistry, {}, workspaceContext, false);
225
+ }, mockedToolRegistry, {}, workspaceContext, {}, false);
225
226
  await client.connect();
226
227
  await client.discover({});
227
228
  expect(mockedToolRegistry.registerTool).toHaveBeenCalledOnce();
@@ -271,7 +272,7 @@ describe('mcp-client', () => {
271
272
  };
272
273
  const client = new McpClient('test-server', {
273
274
  command: 'test-command',
274
- }, mockedToolRegistry, {}, workspaceContext, false);
275
+ }, mockedToolRegistry, {}, workspaceContext, {}, false);
275
276
  await client.connect();
276
277
  await client.discover({});
277
278
  expect(mockedToolRegistry.registerTool).toHaveBeenCalledOnce();
@@ -332,7 +333,7 @@ describe('mcp-client', () => {
332
333
  };
333
334
  const client = new McpClient('test-server', {
334
335
  command: 'test-command',
335
- }, mockedToolRegistry, mockedPromptRegistry, workspaceContext, false);
336
+ }, mockedToolRegistry, mockedPromptRegistry, workspaceContext, {}, false);
336
337
  await client.connect();
337
338
  await client.discover({});
338
339
  expect(mockedToolRegistry.registerTool).toHaveBeenCalledOnce();
@@ -343,6 +344,250 @@ describe('mcp-client', () => {
343
344
  expect(mockedPromptRegistry.removePromptsByServer).toHaveBeenCalledOnce();
344
345
  });
345
346
  });
347
+ describe('Dynamic Tool Updates', () => {
348
+ it('should set up notification handler if server supports tool list changes', async () => {
349
+ const mockedClient = {
350
+ connect: vi.fn(),
351
+ getStatus: vi.fn(),
352
+ registerCapabilities: vi.fn(),
353
+ setRequestHandler: vi.fn(),
354
+ // Capability enables the listener
355
+ getServerCapabilities: vi
356
+ .fn()
357
+ .mockReturnValue({ tools: { listChanged: true } }),
358
+ setNotificationHandler: vi.fn(),
359
+ listTools: vi.fn().mockResolvedValue({ tools: [] }),
360
+ listPrompts: vi.fn().mockResolvedValue({ prompts: [] }),
361
+ request: vi.fn().mockResolvedValue({}),
362
+ };
363
+ vi.mocked(ClientLib.Client).mockReturnValue(mockedClient);
364
+ vi.spyOn(SdkClientStdioLib, 'StdioClientTransport').mockReturnValue({});
365
+ const client = new McpClient('test-server', { command: 'test-command' }, {}, {}, workspaceContext, {}, false);
366
+ await client.connect();
367
+ expect(mockedClient.setNotificationHandler).toHaveBeenCalledWith(ToolListChangedNotificationSchema, expect.any(Function));
368
+ });
369
+ it('should NOT set up notification handler if server lacks capability', async () => {
370
+ const mockedClient = {
371
+ connect: vi.fn(),
372
+ getServerCapabilities: vi.fn().mockReturnValue({ tools: {} }), // No listChanged
373
+ setNotificationHandler: vi.fn(),
374
+ request: vi.fn().mockResolvedValue({}),
375
+ registerCapabilities: vi.fn().mockResolvedValue({}),
376
+ setRequestHandler: vi.fn().mockResolvedValue({}),
377
+ };
378
+ vi.mocked(ClientLib.Client).mockReturnValue(mockedClient);
379
+ vi.spyOn(SdkClientStdioLib, 'StdioClientTransport').mockReturnValue({});
380
+ const client = new McpClient('test-server', { command: 'test-command' }, {}, {}, workspaceContext, {}, false);
381
+ await client.connect();
382
+ expect(mockedClient.setNotificationHandler).not.toHaveBeenCalled();
383
+ });
384
+ it('should refresh tools and notify manager when notification is received', async () => {
385
+ // Setup mocks
386
+ const mockedClient = {
387
+ connect: vi.fn(),
388
+ getServerCapabilities: vi
389
+ .fn()
390
+ .mockReturnValue({ tools: { listChanged: true } }),
391
+ setNotificationHandler: vi.fn(),
392
+ listTools: vi.fn().mockResolvedValue({
393
+ tools: [
394
+ {
395
+ name: 'newTool',
396
+ inputSchema: { type: 'object', properties: {} },
397
+ },
398
+ ],
399
+ }),
400
+ listPrompts: vi.fn().mockResolvedValue({ prompts: [] }),
401
+ request: vi.fn().mockResolvedValue({}),
402
+ registerCapabilities: vi.fn().mockResolvedValue({}),
403
+ setRequestHandler: vi.fn().mockResolvedValue({}),
404
+ };
405
+ vi.mocked(ClientLib.Client).mockReturnValue(mockedClient);
406
+ vi.spyOn(SdkClientStdioLib, 'StdioClientTransport').mockReturnValue({});
407
+ const mockedToolRegistry = {
408
+ removeMcpToolsByServer: vi.fn(),
409
+ registerTool: vi.fn(),
410
+ sortTools: vi.fn(),
411
+ getMessageBus: vi.fn().mockReturnValue(undefined),
412
+ };
413
+ const onToolsUpdatedSpy = vi.fn().mockResolvedValue(undefined);
414
+ // Initialize client with onToolsUpdated callback
415
+ const client = new McpClient('test-server', { command: 'test-command' }, mockedToolRegistry, {}, workspaceContext, {}, false, onToolsUpdatedSpy);
416
+ // 1. Connect (sets up listener)
417
+ await client.connect();
418
+ // 2. Extract the callback passed to setNotificationHandler
419
+ const notificationCallback = mockedClient.setNotificationHandler.mock.calls[0][1];
420
+ // 3. Trigger the notification manually
421
+ await notificationCallback();
422
+ // 4. Assertions
423
+ // It should clear old tools
424
+ expect(mockedToolRegistry.removeMcpToolsByServer).toHaveBeenCalledWith('test-server');
425
+ // It should fetch new tools (listTools called inside discoverTools)
426
+ expect(mockedClient.listTools).toHaveBeenCalled();
427
+ // It should register the new tool
428
+ expect(mockedToolRegistry.registerTool).toHaveBeenCalled();
429
+ // It should notify the manager
430
+ expect(onToolsUpdatedSpy).toHaveBeenCalled();
431
+ // It should emit feedback event
432
+ expect(coreEvents.emitFeedback).toHaveBeenCalledWith('info', 'Tools updated for server: test-server');
433
+ });
434
+ it('should handle errors during tool refresh gracefully', async () => {
435
+ const mockedClient = {
436
+ connect: vi.fn(),
437
+ getServerCapabilities: vi
438
+ .fn()
439
+ .mockReturnValue({ tools: { listChanged: true } }),
440
+ setNotificationHandler: vi.fn(),
441
+ // Simulate error during discovery
442
+ listTools: vi.fn().mockRejectedValue(new Error('Network blip')),
443
+ request: vi.fn().mockResolvedValue({}),
444
+ registerCapabilities: vi.fn().mockResolvedValue({}),
445
+ setRequestHandler: vi.fn().mockResolvedValue({}),
446
+ };
447
+ vi.mocked(ClientLib.Client).mockReturnValue(mockedClient);
448
+ vi.spyOn(SdkClientStdioLib, 'StdioClientTransport').mockReturnValue({});
449
+ const mockedToolRegistry = {
450
+ removeMcpToolsByServer: vi.fn(),
451
+ getMessageBus: vi.fn().mockReturnValue(undefined),
452
+ };
453
+ const client = new McpClient('test-server', { command: 'test-command' }, mockedToolRegistry, {}, workspaceContext, {}, false);
454
+ await client.connect();
455
+ const notificationCallback = mockedClient.setNotificationHandler.mock.calls[0][1];
456
+ // Trigger notification - should fail internally but catch the error
457
+ await notificationCallback();
458
+ // Should try to remove tools
459
+ expect(mockedToolRegistry.removeMcpToolsByServer).toHaveBeenCalled();
460
+ // Should NOT emit success feedback
461
+ expect(coreEvents.emitFeedback).not.toHaveBeenCalledWith('info', expect.stringContaining('Tools updated'));
462
+ });
463
+ it('should handle concurrent updates from multiple servers', async () => {
464
+ const createMockSdkClient = (toolName) => ({
465
+ connect: vi.fn(),
466
+ getServerCapabilities: vi
467
+ .fn()
468
+ .mockReturnValue({ tools: { listChanged: true } }),
469
+ setNotificationHandler: vi.fn(),
470
+ listTools: vi.fn().mockResolvedValue({
471
+ tools: [
472
+ {
473
+ name: toolName,
474
+ inputSchema: { type: 'object', properties: {} },
475
+ },
476
+ ],
477
+ }),
478
+ listPrompts: vi.fn().mockResolvedValue({ prompts: [] }),
479
+ request: vi.fn().mockResolvedValue({}),
480
+ registerCapabilities: vi.fn().mockResolvedValue({}),
481
+ setRequestHandler: vi.fn().mockResolvedValue({}),
482
+ });
483
+ const mockClientA = createMockSdkClient('tool-from-A');
484
+ const mockClientB = createMockSdkClient('tool-from-B');
485
+ vi.mocked(ClientLib.Client)
486
+ .mockReturnValueOnce(mockClientA)
487
+ .mockReturnValueOnce(mockClientB);
488
+ vi.spyOn(SdkClientStdioLib, 'StdioClientTransport').mockReturnValue({});
489
+ const mockedToolRegistry = {
490
+ removeMcpToolsByServer: vi.fn(),
491
+ registerTool: vi.fn(),
492
+ sortTools: vi.fn(),
493
+ getMessageBus: vi.fn().mockReturnValue(undefined),
494
+ };
495
+ const onToolsUpdatedSpy = vi.fn().mockResolvedValue(undefined);
496
+ const clientA = new McpClient('server-A', { command: 'cmd-a' }, mockedToolRegistry, {}, workspaceContext, {}, false, onToolsUpdatedSpy);
497
+ const clientB = new McpClient('server-B', { command: 'cmd-b' }, mockedToolRegistry, {}, workspaceContext, {}, false, onToolsUpdatedSpy);
498
+ await clientA.connect();
499
+ await clientB.connect();
500
+ const handlerA = mockClientA.setNotificationHandler.mock.calls[0][1];
501
+ const handlerB = mockClientB.setNotificationHandler.mock.calls[0][1];
502
+ // Trigger burst updates simultaneously
503
+ await Promise.all([handlerA(), handlerB()]);
504
+ expect(mockedToolRegistry.removeMcpToolsByServer).toHaveBeenCalledWith('server-A');
505
+ expect(mockedToolRegistry.removeMcpToolsByServer).toHaveBeenCalledWith('server-B');
506
+ // Verify fetching happened on both clients
507
+ expect(mockClientA.listTools).toHaveBeenCalled();
508
+ expect(mockClientB.listTools).toHaveBeenCalled();
509
+ // Verify tools from both servers were registered (2 total calls)
510
+ expect(mockedToolRegistry.registerTool).toHaveBeenCalledTimes(2);
511
+ // Verify the update callback was triggered for both
512
+ expect(onToolsUpdatedSpy).toHaveBeenCalledTimes(2);
513
+ });
514
+ it('should abort discovery and log error if timeout is exceeded during refresh', async () => {
515
+ vi.useFakeTimers();
516
+ const mockedClient = {
517
+ connect: vi.fn(),
518
+ getServerCapabilities: vi
519
+ .fn()
520
+ .mockReturnValue({ tools: { listChanged: true } }),
521
+ setNotificationHandler: vi.fn(),
522
+ // Mock listTools to simulate a long running process that respects the abort signal
523
+ listTools: vi.fn().mockImplementation(async (params, options) => new Promise((resolve, reject) => {
524
+ if (options?.signal?.aborted) {
525
+ return reject(new Error('Operation aborted'));
526
+ }
527
+ options?.signal?.addEventListener('abort', () => {
528
+ reject(new Error('Operation aborted'));
529
+ });
530
+ // Intentionally do not resolve immediately to simulate lag
531
+ })),
532
+ listPrompts: vi.fn().mockResolvedValue({ prompts: [] }),
533
+ request: vi.fn().mockResolvedValue({}),
534
+ registerCapabilities: vi.fn().mockResolvedValue({}),
535
+ setRequestHandler: vi.fn().mockResolvedValue({}),
536
+ };
537
+ vi.mocked(ClientLib.Client).mockReturnValue(mockedClient);
538
+ vi.spyOn(SdkClientStdioLib, 'StdioClientTransport').mockReturnValue({});
539
+ const mockedToolRegistry = {
540
+ removeMcpToolsByServer: vi.fn(),
541
+ registerTool: vi.fn(),
542
+ sortTools: vi.fn(),
543
+ getMessageBus: vi.fn().mockReturnValue(undefined),
544
+ };
545
+ const client = new McpClient('test-server',
546
+ // Set a short timeout
547
+ { command: 'test-command', timeout: 100 }, mockedToolRegistry, {}, workspaceContext, {}, false);
548
+ await client.connect();
549
+ const notificationCallback = mockedClient.setNotificationHandler.mock.calls[0][1];
550
+ const refreshPromise = notificationCallback();
551
+ vi.advanceTimersByTime(150);
552
+ await refreshPromise;
553
+ expect(mockedClient.listTools).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
554
+ signal: expect.any(AbortSignal),
555
+ }));
556
+ expect(mockedToolRegistry.registerTool).not.toHaveBeenCalled();
557
+ vi.useRealTimers();
558
+ });
559
+ it('should pass abort signal to onToolsUpdated callback', async () => {
560
+ const mockedClient = {
561
+ connect: vi.fn(),
562
+ getServerCapabilities: vi
563
+ .fn()
564
+ .mockReturnValue({ tools: { listChanged: true } }),
565
+ setNotificationHandler: vi.fn(),
566
+ listTools: vi.fn().mockResolvedValue({ tools: [] }),
567
+ listPrompts: vi.fn().mockResolvedValue({ prompts: [] }),
568
+ request: vi.fn().mockResolvedValue({}),
569
+ registerCapabilities: vi.fn().mockResolvedValue({}),
570
+ setRequestHandler: vi.fn().mockResolvedValue({}),
571
+ };
572
+ vi.mocked(ClientLib.Client).mockReturnValue(mockedClient);
573
+ vi.spyOn(SdkClientStdioLib, 'StdioClientTransport').mockReturnValue({});
574
+ const mockedToolRegistry = {
575
+ removeMcpToolsByServer: vi.fn(),
576
+ registerTool: vi.fn(),
577
+ sortTools: vi.fn(),
578
+ getMessageBus: vi.fn().mockReturnValue(undefined),
579
+ };
580
+ const onToolsUpdatedSpy = vi.fn().mockResolvedValue(undefined);
581
+ const client = new McpClient('test-server', { command: 'test-command' }, mockedToolRegistry, {}, workspaceContext, {}, false, onToolsUpdatedSpy);
582
+ await client.connect();
583
+ const notificationCallback = mockedClient.setNotificationHandler.mock.calls[0][1];
584
+ await notificationCallback();
585
+ expect(onToolsUpdatedSpy).toHaveBeenCalledWith(expect.any(AbortSignal));
586
+ // Verify the signal passed was not aborted (happy path)
587
+ const signal = onToolsUpdatedSpy.mock.calls[0][0];
588
+ expect(signal.aborted).toBe(false);
589
+ });
590
+ });
346
591
  describe('appendMcpServerCommand', () => {
347
592
  it('should do nothing if no MCP servers or command are configured', () => {
348
593
  const out = populateMcpServerCommand({}, undefined);
@@ -369,19 +614,23 @@ describe('mcp-client', () => {
369
614
  httpUrl: 'http://test-server',
370
615
  }, false);
371
616
  expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
372
- expect(transport).toHaveProperty('_url', new URL('http://test-server/'));
617
+ expect(transport).toMatchObject({
618
+ _url: new URL('http://test-server'),
619
+ _requestInit: { headers: {} },
620
+ });
373
621
  });
374
622
  it('with headers', async () => {
375
- // We need this to be an any type because we dig into its private state.
376
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
377
623
  const transport = await createTransport('test-server', {
378
624
  httpUrl: 'http://test-server',
379
625
  headers: { Authorization: 'derp' },
380
626
  }, false);
381
627
  expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
382
- expect(transport).toHaveProperty('_url', new URL('http://test-server/'));
383
- const authHeader = transport._requestInit?.headers?.['Authorization'];
384
- expect(authHeader).toBe('derp');
628
+ expect(transport).toMatchObject({
629
+ _url: new URL('http://test-server'),
630
+ _requestInit: {
631
+ headers: { Authorization: 'derp' },
632
+ },
633
+ });
385
634
  });
386
635
  });
387
636
  describe('should connect via url', () => {
@@ -389,20 +638,96 @@ describe('mcp-client', () => {
389
638
  const transport = await createTransport('test-server', {
390
639
  url: 'http://test-server',
391
640
  }, false);
392
- expect(transport).toBeInstanceOf(SSEClientTransport);
393
- expect(transport).toHaveProperty('_url', new URL('http://test-server/'));
641
+ expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
642
+ expect(transport).toMatchObject({
643
+ _url: new URL('http://test-server'),
644
+ _requestInit: { headers: {} },
645
+ });
394
646
  });
395
647
  it('with headers', async () => {
396
- // We need this to be an any type because we dig into its private state.
397
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
398
648
  const transport = await createTransport('test-server', {
399
649
  url: 'http://test-server',
400
650
  headers: { Authorization: 'derp' },
401
651
  }, false);
652
+ expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
653
+ expect(transport).toMatchObject({
654
+ _url: new URL('http://test-server'),
655
+ _requestInit: {
656
+ headers: { Authorization: 'derp' },
657
+ },
658
+ });
659
+ });
660
+ it('with type="http" creates StreamableHTTPClientTransport', async () => {
661
+ const transport = await createTransport('test-server', {
662
+ url: 'http://test-server',
663
+ type: 'http',
664
+ }, false);
665
+ expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
666
+ expect(transport).toMatchObject({
667
+ _url: new URL('http://test-server'),
668
+ _requestInit: { headers: {} },
669
+ });
670
+ });
671
+ it('with type="sse" creates SSEClientTransport', async () => {
672
+ const transport = await createTransport('test-server', {
673
+ url: 'http://test-server',
674
+ type: 'sse',
675
+ }, false);
676
+ expect(transport).toBeInstanceOf(SSEClientTransport);
677
+ expect(transport).toMatchObject({
678
+ _url: new URL('http://test-server'),
679
+ _requestInit: { headers: {} },
680
+ });
681
+ });
682
+ it('without type defaults to StreamableHTTPClientTransport', async () => {
683
+ const transport = await createTransport('test-server', {
684
+ url: 'http://test-server',
685
+ }, false);
686
+ expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
687
+ expect(transport).toMatchObject({
688
+ _url: new URL('http://test-server'),
689
+ _requestInit: { headers: {} },
690
+ });
691
+ });
692
+ it('with type="http" and headers applies headers correctly', async () => {
693
+ const transport = await createTransport('test-server', {
694
+ url: 'http://test-server',
695
+ type: 'http',
696
+ headers: { Authorization: 'Bearer token' },
697
+ }, false);
698
+ expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
699
+ expect(transport).toMatchObject({
700
+ _url: new URL('http://test-server'),
701
+ _requestInit: {
702
+ headers: { Authorization: 'Bearer token' },
703
+ },
704
+ });
705
+ });
706
+ it('with type="sse" and headers applies headers correctly', async () => {
707
+ const transport = await createTransport('test-server', {
708
+ url: 'http://test-server',
709
+ type: 'sse',
710
+ headers: { 'X-API-Key': 'key123' },
711
+ }, false);
402
712
  expect(transport).toBeInstanceOf(SSEClientTransport);
403
- expect(transport).toHaveProperty('_url', new URL('http://test-server/'));
404
- const authHeader = transport._requestInit?.headers?.['Authorization'];
405
- expect(authHeader).toBe('derp');
713
+ expect(transport).toMatchObject({
714
+ _url: new URL('http://test-server'),
715
+ _requestInit: {
716
+ headers: { 'X-API-Key': 'key123' },
717
+ },
718
+ });
719
+ });
720
+ it('httpUrl takes priority over url when both are present', async () => {
721
+ const transport = await createTransport('test-server', {
722
+ httpUrl: 'http://test-server-http',
723
+ url: 'http://test-server-url',
724
+ }, false);
725
+ // httpUrl should take priority and create HTTP transport
726
+ expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
727
+ expect(transport).toMatchObject({
728
+ _url: new URL('http://test-server-http'),
729
+ _requestInit: { headers: {} },
730
+ });
406
731
  });
407
732
  });
408
733
  it('should connect via command', async () => {
@@ -492,6 +817,7 @@ describe('mcp-client', () => {
492
817
  it('should use GoogleCredentialProvider with SSE transport', async () => {
493
818
  const transport = await createTransport('test-server', {
494
819
  url: 'http://test.googleapis.com',
820
+ type: 'sse',
495
821
  authProviderType: AuthProviderType.GOOGLE_CREDENTIALS,
496
822
  oauth: {
497
823
  scopes: ['scope1'],
@@ -614,7 +940,7 @@ describe('connectToMcpServer with OAuth', () => {
614
940
  const authUrl = 'http://auth.example.com/auth';
615
941
  const tokenUrl = 'http://auth.example.com/token';
616
942
  const wwwAuthHeader = `Bearer realm="test", resource_metadata="http://test-server.com/.well-known/oauth-protected-resource"`;
617
- vi.mocked(mockedClient.connect).mockRejectedValueOnce(new Error(`401 Unauthorized\nwww-authenticate: ${wwwAuthHeader}`));
943
+ vi.mocked(mockedClient.connect).mockRejectedValueOnce(new StreamableHTTPError(401, `Unauthorized\nwww-authenticate: ${wwwAuthHeader}`));
618
944
  vi.mocked(OAuthUtils.discoverOAuthConfig).mockResolvedValue({
619
945
  authorizationUrl: authUrl,
620
946
  tokenUrl,
@@ -627,7 +953,7 @@ describe('connectToMcpServer with OAuth', () => {
627
953
  capturedTransport = transport;
628
954
  return Promise.resolve();
629
955
  });
630
- const client = await connectToMcpServer('test-server', { httpUrl: serverUrl }, false, workspaceContext);
956
+ const client = await connectToMcpServer('test-server', { httpUrl: serverUrl, oauth: { enabled: true } }, false, workspaceContext);
631
957
  expect(client).toBe(mockedClient);
632
958
  expect(mockedClient.connect).toHaveBeenCalledTimes(2);
633
959
  expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
@@ -638,7 +964,7 @@ describe('connectToMcpServer with OAuth', () => {
638
964
  const serverUrl = 'http://test-server.com';
639
965
  const authUrl = 'http://auth.example.com/auth';
640
966
  const tokenUrl = 'http://auth.example.com/token';
641
- vi.mocked(mockedClient.connect).mockRejectedValueOnce(new Error('401 Unauthorized'));
967
+ vi.mocked(mockedClient.connect).mockRejectedValueOnce(new StreamableHTTPError(401, 'Unauthorized'));
642
968
  vi.mocked(OAuthUtils.discoverOAuthConfig).mockResolvedValue({
643
969
  authorizationUrl: authUrl,
644
970
  tokenUrl,
@@ -652,7 +978,7 @@ describe('connectToMcpServer with OAuth', () => {
652
978
  capturedTransport = transport;
653
979
  return Promise.resolve();
654
980
  });
655
- const client = await connectToMcpServer('test-server', { httpUrl: serverUrl }, false, workspaceContext);
981
+ const client = await connectToMcpServer('test-server', { httpUrl: serverUrl, oauth: { enabled: true } }, false, workspaceContext);
656
982
  expect(client).toBe(mockedClient);
657
983
  expect(mockedClient.connect).toHaveBeenCalledTimes(2);
658
984
  expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
@@ -661,4 +987,118 @@ describe('connectToMcpServer with OAuth', () => {
661
987
  expect(authHeader).toBe('Bearer test-access-token-from-discovery');
662
988
  });
663
989
  });
990
+ describe('connectToMcpServer - HTTP→SSE fallback', () => {
991
+ let mockedClient;
992
+ let workspaceContext;
993
+ let testWorkspace;
994
+ beforeEach(() => {
995
+ mockedClient = {
996
+ connect: vi.fn(),
997
+ close: vi.fn(),
998
+ registerCapabilities: vi.fn(),
999
+ setRequestHandler: vi.fn(),
1000
+ onclose: vi.fn(),
1001
+ notification: vi.fn(),
1002
+ };
1003
+ vi.mocked(ClientLib.Client).mockImplementation(() => mockedClient);
1004
+ testWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-agent-test-'));
1005
+ workspaceContext = new WorkspaceContext(testWorkspace);
1006
+ vi.spyOn(console, 'log').mockImplementation(() => { });
1007
+ vi.spyOn(console, 'warn').mockImplementation(() => { });
1008
+ vi.spyOn(console, 'error').mockImplementation(() => { });
1009
+ });
1010
+ afterEach(() => {
1011
+ vi.clearAllMocks();
1012
+ });
1013
+ it('should NOT trigger fallback when type="http" is explicit', async () => {
1014
+ vi.mocked(mockedClient.connect).mockRejectedValueOnce(new Error('Connection failed'));
1015
+ await expect(connectToMcpServer('test-server', { url: 'http://test-server', type: 'http' }, false, workspaceContext)).rejects.toThrow('Connection failed');
1016
+ // Should only try once (no fallback)
1017
+ expect(mockedClient.connect).toHaveBeenCalledTimes(1);
1018
+ });
1019
+ it('should NOT trigger fallback when type="sse" is explicit', async () => {
1020
+ vi.mocked(mockedClient.connect).mockRejectedValueOnce(new Error('Connection failed'));
1021
+ await expect(connectToMcpServer('test-server', { url: 'http://test-server', type: 'sse' }, false, workspaceContext)).rejects.toThrow('Connection failed');
1022
+ // Should only try once (no fallback)
1023
+ expect(mockedClient.connect).toHaveBeenCalledTimes(1);
1024
+ });
1025
+ it('should trigger fallback when url provided without type and HTTP fails', async () => {
1026
+ vi.mocked(mockedClient.connect)
1027
+ .mockRejectedValueOnce(new StreamableHTTPError(500, 'Server error'))
1028
+ .mockResolvedValueOnce(undefined);
1029
+ const client = await connectToMcpServer('test-server', { url: 'http://test-server' }, false, workspaceContext);
1030
+ expect(client).toBe(mockedClient);
1031
+ // First HTTP attempt fails, second SSE attempt succeeds
1032
+ expect(mockedClient.connect).toHaveBeenCalledTimes(2);
1033
+ });
1034
+ it('should throw original HTTP error when both HTTP and SSE fail (non-401)', async () => {
1035
+ const httpError = new StreamableHTTPError(500, 'Server error');
1036
+ const sseError = new Error('SSE connection failed');
1037
+ vi.mocked(mockedClient.connect)
1038
+ .mockRejectedValueOnce(httpError)
1039
+ .mockRejectedValueOnce(sseError);
1040
+ await expect(connectToMcpServer('test-server', { url: 'http://test-server' }, false, workspaceContext)).rejects.toThrow('Server error');
1041
+ expect(mockedClient.connect).toHaveBeenCalledTimes(2);
1042
+ });
1043
+ it('should handle HTTP 404 followed by SSE success', async () => {
1044
+ vi.mocked(mockedClient.connect)
1045
+ .mockRejectedValueOnce(new StreamableHTTPError(404, 'Not Found'))
1046
+ .mockResolvedValueOnce(undefined);
1047
+ const client = await connectToMcpServer('test-server', { url: 'http://test-server' }, false, workspaceContext);
1048
+ expect(client).toBe(mockedClient);
1049
+ expect(mockedClient.connect).toHaveBeenCalledTimes(2);
1050
+ });
1051
+ });
1052
+ describe('connectToMcpServer - OAuth with transport fallback', () => {
1053
+ let mockedClient;
1054
+ let workspaceContext;
1055
+ let testWorkspace;
1056
+ let mockAuthProvider;
1057
+ let mockTokenStorage;
1058
+ beforeEach(() => {
1059
+ mockedClient = {
1060
+ connect: vi.fn(),
1061
+ close: vi.fn(),
1062
+ registerCapabilities: vi.fn(),
1063
+ setRequestHandler: vi.fn(),
1064
+ onclose: vi.fn(),
1065
+ notification: vi.fn(),
1066
+ };
1067
+ vi.mocked(ClientLib.Client).mockImplementation(() => mockedClient);
1068
+ testWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-agent-test-'));
1069
+ workspaceContext = new WorkspaceContext(testWorkspace);
1070
+ vi.spyOn(console, 'log').mockImplementation(() => { });
1071
+ vi.spyOn(console, 'warn').mockImplementation(() => { });
1072
+ vi.spyOn(console, 'error').mockImplementation(() => { });
1073
+ mockTokenStorage = {
1074
+ getCredentials: vi.fn().mockResolvedValue({ clientId: 'test-client' }),
1075
+ };
1076
+ vi.mocked(MCPOAuthTokenStorage).mockReturnValue(mockTokenStorage);
1077
+ mockAuthProvider = {
1078
+ authenticate: vi.fn().mockResolvedValue(undefined),
1079
+ getValidToken: vi.fn().mockResolvedValue('test-access-token'),
1080
+ tokenStorage: mockTokenStorage,
1081
+ };
1082
+ vi.mocked(MCPOAuthProvider).mockReturnValue(mockAuthProvider);
1083
+ vi.mocked(OAuthUtils.discoverOAuthConfig).mockResolvedValue({
1084
+ authorizationUrl: 'http://auth.example.com/auth',
1085
+ tokenUrl: 'http://auth.example.com/token',
1086
+ scopes: ['test-scope'],
1087
+ });
1088
+ });
1089
+ afterEach(() => {
1090
+ vi.clearAllMocks();
1091
+ });
1092
+ it('should handle HTTP 404 → SSE 401 → OAuth → SSE+OAuth succeeds', async () => {
1093
+ // Tests that OAuth flow works when SSE (not HTTP) requires auth
1094
+ vi.mocked(mockedClient.connect)
1095
+ .mockRejectedValueOnce(new StreamableHTTPError(404, 'Not Found'))
1096
+ .mockRejectedValueOnce(new StreamableHTTPError(401, 'Unauthorized'))
1097
+ .mockResolvedValueOnce(undefined);
1098
+ const client = await connectToMcpServer('test-server', { url: 'http://test-server', oauth: { enabled: true } }, false, workspaceContext);
1099
+ expect(client).toBe(mockedClient);
1100
+ expect(mockedClient.connect).toHaveBeenCalledTimes(3);
1101
+ expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
1102
+ });
1103
+ });
664
1104
  //# sourceMappingURL=mcp-client.test.js.map