@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.
- package/dist/google-gemini-cli-core-0.21.0-nightly.20251202.2d935b379.tgz +0 -0
- package/dist/src/code_assist/oauth2.d.ts +2 -0
- package/dist/src/code_assist/oauth2.js +28 -12
- package/dist/src/code_assist/oauth2.js.map +1 -1
- package/dist/src/code_assist/oauth2.test.js +40 -2
- package/dist/src/code_assist/oauth2.test.js.map +1 -1
- package/dist/src/commands/restore.d.ts +19 -0
- package/dist/src/commands/restore.js +45 -0
- package/dist/src/commands/restore.js.map +1 -0
- package/dist/src/commands/restore.test.d.ts +6 -0
- package/dist/src/commands/restore.test.js +136 -0
- package/dist/src/commands/restore.test.js.map +1 -0
- package/dist/src/commands/types.d.ts +41 -0
- package/dist/src/commands/types.js +7 -0
- package/dist/src/commands/types.js.map +1 -0
- package/dist/src/config/config.d.ts +17 -2
- package/dist/src/config/config.js +28 -0
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/core/client.js +2 -1
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/client.test.js +23 -0
- package/dist/src/core/client.test.js.map +1 -1
- package/dist/src/core/geminiChat.test.js +4 -0
- package/dist/src/core/geminiChat.test.js.map +1 -1
- package/dist/src/core/sessionHookTriggers.d.ts +28 -0
- package/dist/src/core/sessionHookTriggers.js +68 -0
- package/dist/src/core/sessionHookTriggers.js.map +1 -0
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/hooks/hookEventHandler.js +51 -0
- package/dist/src/hooks/hookEventHandler.js.map +1 -1
- package/dist/src/hooks/hookRegistry.js +8 -1
- package/dist/src/hooks/hookRegistry.js.map +1 -1
- package/dist/src/hooks/hookRegistry.test.js +1 -0
- package/dist/src/hooks/hookRegistry.test.js.map +1 -1
- package/dist/src/hooks/hookRunner.js +12 -2
- package/dist/src/hooks/hookRunner.js.map +1 -1
- package/dist/src/hooks/hookSystem.test.js +124 -0
- package/dist/src/hooks/hookSystem.test.js.map +1 -1
- package/dist/src/hooks/index.d.ts +3 -1
- package/dist/src/hooks/index.js +3 -0
- package/dist/src/hooks/index.js.map +1 -1
- package/dist/src/hooks/types.d.ts +1 -2
- package/dist/src/hooks/types.js +0 -1
- package/dist/src/hooks/types.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/output/json-formatter.d.ts +2 -2
- package/dist/src/output/json-formatter.js +6 -3
- package/dist/src/output/json-formatter.js.map +1 -1
- package/dist/src/output/json-formatter.test.js +35 -9
- package/dist/src/output/json-formatter.test.js.map +1 -1
- package/dist/src/output/types.d.ts +1 -0
- package/dist/src/output/types.js.map +1 -1
- package/dist/src/services/chatCompressionService.js +12 -0
- package/dist/src/services/chatCompressionService.js.map +1 -1
- package/dist/src/services/chatCompressionService.test.js +2 -0
- package/dist/src/services/chatCompressionService.test.js.map +1 -1
- package/dist/src/services/shellExecutionService.js +5 -18
- package/dist/src/services/shellExecutionService.js.map +1 -1
- package/dist/src/services/shellExecutionService.test.js +29 -2
- package/dist/src/services/shellExecutionService.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +1 -0
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +20 -0
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +49 -0
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +1 -0
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +3 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
- package/dist/src/telemetry/config.js +2 -0
- package/dist/src/telemetry/config.js.map +1 -1
- package/dist/src/telemetry/config.test.js +25 -0
- package/dist/src/telemetry/config.test.js.map +1 -1
- package/dist/src/telemetry/gcp-exporters.d.ts +4 -3
- package/dist/src/telemetry/gcp-exporters.js +7 -4
- package/dist/src/telemetry/gcp-exporters.js.map +1 -1
- package/dist/src/telemetry/index.d.ts +1 -1
- package/dist/src/telemetry/index.js +1 -1
- package/dist/src/telemetry/index.js.map +1 -1
- package/dist/src/telemetry/loggers.js +335 -335
- package/dist/src/telemetry/loggers.js.map +1 -1
- package/dist/src/telemetry/loggers.test.js +15 -0
- package/dist/src/telemetry/loggers.test.js.map +1 -1
- package/dist/src/telemetry/sdk.d.ts +9 -2
- package/dist/src/telemetry/sdk.js +126 -16
- package/dist/src/telemetry/sdk.js.map +1 -1
- package/dist/src/telemetry/sdk.test.js +111 -28
- package/dist/src/telemetry/sdk.test.js.map +1 -1
- package/dist/src/telemetry/startupProfiler.test.js +4 -0
- package/dist/src/telemetry/startupProfiler.test.js.map +1 -1
- package/dist/src/telemetry/telemetry.test.js +10 -3
- package/dist/src/telemetry/telemetry.test.js.map +1 -1
- package/dist/src/tools/mcp-client-manager.js +15 -4
- package/dist/src/tools/mcp-client-manager.js.map +1 -1
- package/dist/src/tools/mcp-client.d.ts +17 -2
- package/dist/src/tools/mcp-client.js +319 -166
- package/dist/src/tools/mcp-client.js.map +1 -1
- package/dist/src/tools/mcp-client.test.js +466 -26
- package/dist/src/tools/mcp-client.test.js.map +1 -1
- package/dist/src/tools/modifiable-tool.test.js +22 -13
- package/dist/src/tools/modifiable-tool.test.js.map +1 -1
- package/dist/src/utils/debugLogger.d.ts +3 -0
- package/dist/src/utils/debugLogger.js +27 -0
- package/dist/src/utils/debugLogger.js.map +1 -1
- package/dist/src/utils/editCorrector.test.js +4 -0
- package/dist/src/utils/editCorrector.test.js.map +1 -1
- package/dist/src/utils/editor.d.ts +9 -1
- package/dist/src/utils/editor.js +23 -14
- package/dist/src/utils/editor.js.map +1 -1
- package/dist/src/utils/errors.d.ts +8 -0
- package/dist/src/utils/errors.js +32 -0
- package/dist/src/utils/errors.js.map +1 -1
- package/dist/src/utils/errors.test.d.ts +6 -0
- package/dist/src/utils/errors.test.js +36 -0
- package/dist/src/utils/errors.test.js.map +1 -0
- package/dist/src/utils/nextSpeakerChecker.test.js +4 -0
- package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -1
- package/dist/src/utils/retry.js +38 -5
- package/dist/src/utils/retry.js.map +1 -1
- package/dist/src/utils/retry.test.js +35 -4
- package/dist/src/utils/retry.test.js.map +1 -1
- package/dist/src/utils/terminalSerializer.test.js +17 -0
- package/dist/src/utils/terminalSerializer.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- 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).
|
|
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).
|
|
383
|
-
|
|
384
|
-
|
|
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(
|
|
393
|
-
expect(transport).
|
|
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).
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
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
|
|
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
|