2025-08-06 11:52:29 -04:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2025-10-21 16:35:22 -04:00
|
|
|
import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
|
2025-08-06 11:52:29 -04:00
|
|
|
import { listMcpServers } from './list.js';
|
|
|
|
|
import { loadSettings } from '../../config/settings.js';
|
2025-10-21 16:35:22 -04:00
|
|
|
import { createTransport, debugLogger } from '@google/gemini-cli-core';
|
2025-08-06 11:52:29 -04:00
|
|
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
2025-10-23 11:39:36 -07:00
|
|
|
import { ExtensionStorage } from '../../config/extensions/storage.js';
|
|
|
|
|
import { ExtensionManager } from '../../config/extension-manager.js';
|
2025-08-06 11:52:29 -04:00
|
|
|
|
2025-08-20 10:55:47 +09:00
|
|
|
vi.mock('../../config/settings.js', () => ({
|
|
|
|
|
loadSettings: vi.fn(),
|
|
|
|
|
}));
|
2025-10-23 11:39:36 -07:00
|
|
|
vi.mock('../../config/extensions/storage.js', () => ({
|
2025-09-29 06:53:19 -07:00
|
|
|
ExtensionStorage: {
|
|
|
|
|
getUserExtensionsDir: vi.fn(),
|
|
|
|
|
},
|
2025-08-20 10:55:47 +09:00
|
|
|
}));
|
2025-10-23 11:39:36 -07:00
|
|
|
vi.mock('../../config/extension-manager.js');
|
2025-08-20 10:55:47 +09:00
|
|
|
vi.mock('@google/gemini-cli-core', () => ({
|
|
|
|
|
createTransport: vi.fn(),
|
|
|
|
|
MCPServerStatus: {
|
|
|
|
|
CONNECTED: 'CONNECTED',
|
|
|
|
|
CONNECTING: 'CONNECTING',
|
|
|
|
|
DISCONNECTED: 'DISCONNECTED',
|
|
|
|
|
},
|
|
|
|
|
Storage: vi.fn().mockImplementation((_cwd: string) => ({
|
|
|
|
|
getGlobalSettingsPath: () => '/tmp/gemini/settings.json',
|
|
|
|
|
getWorkspaceSettingsPath: () => '/tmp/gemini/workspace-settings.json',
|
|
|
|
|
getProjectTempDir: () => '/test/home/.gemini/tmp/mocked_hash',
|
|
|
|
|
})),
|
2025-10-14 02:31:39 +09:00
|
|
|
GEMINI_DIR: '.gemini',
|
2025-08-20 10:55:47 +09:00
|
|
|
getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)),
|
2025-10-21 16:35:22 -04:00
|
|
|
debugLogger: {
|
|
|
|
|
log: vi.fn(),
|
|
|
|
|
warn: vi.fn(),
|
|
|
|
|
error: vi.fn(),
|
|
|
|
|
debug: vi.fn(),
|
|
|
|
|
},
|
2025-08-20 10:55:47 +09:00
|
|
|
}));
|
2025-08-06 11:52:29 -04:00
|
|
|
vi.mock('@modelcontextprotocol/sdk/client/index.js');
|
|
|
|
|
|
2025-10-16 07:21:09 -07:00
|
|
|
const mockedGetUserExtensionsDir =
|
|
|
|
|
ExtensionStorage.getUserExtensionsDir as Mock;
|
|
|
|
|
const mockedLoadSettings = loadSettings as Mock;
|
|
|
|
|
const mockedCreateTransport = createTransport as Mock;
|
|
|
|
|
const MockedClient = Client as Mock;
|
2025-10-23 11:39:36 -07:00
|
|
|
const MockedExtensionManager = ExtensionManager as Mock;
|
2025-08-06 11:52:29 -04:00
|
|
|
|
|
|
|
|
interface MockClient {
|
2025-10-16 07:21:09 -07:00
|
|
|
connect: Mock;
|
|
|
|
|
ping: Mock;
|
|
|
|
|
close: Mock;
|
2025-08-06 11:52:29 -04:00
|
|
|
}
|
|
|
|
|
|
2025-10-23 11:39:36 -07:00
|
|
|
interface MockExtensionManager {
|
|
|
|
|
loadExtensions: Mock;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-06 11:52:29 -04:00
|
|
|
interface MockTransport {
|
2025-10-16 07:21:09 -07:00
|
|
|
close: Mock;
|
2025-08-06 11:52:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('mcp list command', () => {
|
|
|
|
|
let mockClient: MockClient;
|
2025-10-23 11:39:36 -07:00
|
|
|
let mockExtensionManager: MockExtensionManager;
|
2025-08-06 11:52:29 -04:00
|
|
|
let mockTransport: MockTransport;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.resetAllMocks();
|
|
|
|
|
|
|
|
|
|
mockTransport = { close: vi.fn() };
|
|
|
|
|
mockClient = {
|
|
|
|
|
connect: vi.fn(),
|
|
|
|
|
ping: vi.fn(),
|
|
|
|
|
close: vi.fn(),
|
|
|
|
|
};
|
2025-10-23 11:39:36 -07:00
|
|
|
mockExtensionManager = {
|
|
|
|
|
loadExtensions: vi.fn(),
|
|
|
|
|
};
|
2025-08-06 11:52:29 -04:00
|
|
|
|
|
|
|
|
MockedClient.mockImplementation(() => mockClient);
|
2025-10-23 11:39:36 -07:00
|
|
|
MockedExtensionManager.mockImplementation(() => mockExtensionManager);
|
2025-08-06 11:52:29 -04:00
|
|
|
mockedCreateTransport.mockResolvedValue(mockTransport);
|
2025-10-23 11:39:36 -07:00
|
|
|
mockExtensionManager.loadExtensions.mockReturnValue([]);
|
2025-10-16 07:21:09 -07:00
|
|
|
mockedGetUserExtensionsDir.mockReturnValue('/mocked/extensions/dir');
|
2025-08-06 11:52:29 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display message when no servers configured', async () => {
|
|
|
|
|
mockedLoadSettings.mockReturnValue({ merged: { mcpServers: {} } });
|
|
|
|
|
|
|
|
|
|
await listMcpServers();
|
|
|
|
|
|
2025-10-21 16:35:22 -04:00
|
|
|
expect(debugLogger.log).toHaveBeenCalledWith('No MCP servers configured.');
|
2025-08-06 11:52:29 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display different server types with connected status', async () => {
|
|
|
|
|
mockedLoadSettings.mockReturnValue({
|
|
|
|
|
merged: {
|
|
|
|
|
mcpServers: {
|
|
|
|
|
'stdio-server': { command: '/path/to/server', args: ['arg1'] },
|
|
|
|
|
'sse-server': { url: 'https://example.com/sse' },
|
|
|
|
|
'http-server': { httpUrl: 'https://example.com/http' },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
mockClient.connect.mockResolvedValue(undefined);
|
|
|
|
|
mockClient.ping.mockResolvedValue(undefined);
|
|
|
|
|
|
|
|
|
|
await listMcpServers();
|
|
|
|
|
|
2025-10-21 16:35:22 -04:00
|
|
|
expect(debugLogger.log).toHaveBeenCalledWith('Configured MCP servers:\n');
|
|
|
|
|
expect(debugLogger.log).toHaveBeenCalledWith(
|
2025-08-06 11:52:29 -04:00
|
|
|
expect.stringContaining(
|
|
|
|
|
'stdio-server: /path/to/server arg1 (stdio) - Connected',
|
|
|
|
|
),
|
|
|
|
|
);
|
2025-10-21 16:35:22 -04:00
|
|
|
expect(debugLogger.log).toHaveBeenCalledWith(
|
2025-08-06 11:52:29 -04:00
|
|
|
expect.stringContaining(
|
|
|
|
|
'sse-server: https://example.com/sse (sse) - Connected',
|
|
|
|
|
),
|
|
|
|
|
);
|
2025-10-21 16:35:22 -04:00
|
|
|
expect(debugLogger.log).toHaveBeenCalledWith(
|
2025-08-06 11:52:29 -04:00
|
|
|
expect.stringContaining(
|
|
|
|
|
'http-server: https://example.com/http (http) - Connected',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display disconnected status when connection fails', async () => {
|
|
|
|
|
mockedLoadSettings.mockReturnValue({
|
|
|
|
|
merged: {
|
|
|
|
|
mcpServers: {
|
|
|
|
|
'test-server': { command: '/test/server' },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
mockClient.connect.mockRejectedValue(new Error('Connection failed'));
|
|
|
|
|
|
|
|
|
|
await listMcpServers();
|
|
|
|
|
|
2025-10-21 16:35:22 -04:00
|
|
|
expect(debugLogger.log).toHaveBeenCalledWith(
|
2025-08-06 11:52:29 -04:00
|
|
|
expect.stringContaining(
|
|
|
|
|
'test-server: /test/server (stdio) - Disconnected',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should merge extension servers with config servers', async () => {
|
|
|
|
|
mockedLoadSettings.mockReturnValue({
|
|
|
|
|
merged: {
|
|
|
|
|
mcpServers: { 'config-server': { command: '/config/server' } },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-23 11:39:36 -07:00
|
|
|
mockExtensionManager.loadExtensions.mockReturnValue([
|
2025-08-06 11:52:29 -04:00
|
|
|
{
|
2025-10-08 07:31:41 -07:00
|
|
|
name: 'test-extension',
|
|
|
|
|
mcpServers: { 'extension-server': { command: '/ext/server' } },
|
2025-08-06 11:52:29 -04:00
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
mockClient.connect.mockResolvedValue(undefined);
|
|
|
|
|
mockClient.ping.mockResolvedValue(undefined);
|
|
|
|
|
|
|
|
|
|
await listMcpServers();
|
|
|
|
|
|
2025-10-21 16:35:22 -04:00
|
|
|
expect(debugLogger.log).toHaveBeenCalledWith(
|
2025-08-06 11:52:29 -04:00
|
|
|
expect.stringContaining(
|
|
|
|
|
'config-server: /config/server (stdio) - Connected',
|
|
|
|
|
),
|
|
|
|
|
);
|
2025-10-21 16:35:22 -04:00
|
|
|
expect(debugLogger.log).toHaveBeenCalledWith(
|
2025-08-06 11:52:29 -04:00
|
|
|
expect.stringContaining(
|
2025-10-16 07:21:09 -07:00
|
|
|
'extension-server (from test-extension): /ext/server (stdio) - Connected',
|
2025-08-06 11:52:29 -04:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|