|
|
@@ -0,0 +1,193 @@
|
|
|
+import type { McpHub as McpHubType } from '../McpHub'
|
|
|
+import type { ClineProvider } from '../../../core/webview/ClineProvider'
|
|
|
+import type { ExtensionContext, Uri } from 'vscode'
|
|
|
+import type { McpConnection } from '../McpHub'
|
|
|
+
|
|
|
+const vscode = require('vscode')
|
|
|
+const fs = require('fs/promises')
|
|
|
+const { McpHub } = require('../McpHub')
|
|
|
+
|
|
|
+jest.mock('vscode')
|
|
|
+jest.mock('fs/promises')
|
|
|
+jest.mock('../../../core/webview/ClineProvider')
|
|
|
+
|
|
|
+describe('McpHub', () => {
|
|
|
+ let mcpHub: McpHubType
|
|
|
+ let mockProvider: Partial<ClineProvider>
|
|
|
+ const mockSettingsPath = '/mock/settings/path/cline_mcp_settings.json'
|
|
|
+
|
|
|
+ beforeEach(() => {
|
|
|
+ jest.clearAllMocks()
|
|
|
+
|
|
|
+ const mockUri: Uri = {
|
|
|
+ scheme: 'file',
|
|
|
+ authority: '',
|
|
|
+ path: '/test/path',
|
|
|
+ query: '',
|
|
|
+ fragment: '',
|
|
|
+ fsPath: '/test/path',
|
|
|
+ with: jest.fn(),
|
|
|
+ toJSON: jest.fn()
|
|
|
+ }
|
|
|
+
|
|
|
+ mockProvider = {
|
|
|
+ ensureSettingsDirectoryExists: jest.fn().mockResolvedValue('/mock/settings/path'),
|
|
|
+ ensureMcpServersDirectoryExists: jest.fn().mockResolvedValue('/mock/settings/path'),
|
|
|
+ postMessageToWebview: jest.fn(),
|
|
|
+ context: {
|
|
|
+ subscriptions: [],
|
|
|
+ workspaceState: {} as any,
|
|
|
+ globalState: {} as any,
|
|
|
+ secrets: {} as any,
|
|
|
+ extensionUri: mockUri,
|
|
|
+ extensionPath: '/test/path',
|
|
|
+ storagePath: '/test/storage',
|
|
|
+ globalStoragePath: '/test/global-storage',
|
|
|
+ environmentVariableCollection: {} as any,
|
|
|
+ extension: {
|
|
|
+ id: 'test-extension',
|
|
|
+ extensionUri: mockUri,
|
|
|
+ extensionPath: '/test/path',
|
|
|
+ extensionKind: 1,
|
|
|
+ isActive: true,
|
|
|
+ packageJSON: {
|
|
|
+ version: '1.0.0'
|
|
|
+ },
|
|
|
+ activate: jest.fn(),
|
|
|
+ exports: undefined
|
|
|
+ } as any,
|
|
|
+ asAbsolutePath: (path: string) => path,
|
|
|
+ storageUri: mockUri,
|
|
|
+ globalStorageUri: mockUri,
|
|
|
+ logUri: mockUri,
|
|
|
+ extensionMode: 1,
|
|
|
+ logPath: '/test/path',
|
|
|
+ languageModelAccessInformation: {} as any
|
|
|
+ } as ExtensionContext
|
|
|
+ }
|
|
|
+
|
|
|
+ // Mock fs.readFile for initial settings
|
|
|
+ ;(fs.readFile as jest.Mock).mockResolvedValue(JSON.stringify({
|
|
|
+ mcpServers: {
|
|
|
+ 'test-server': {
|
|
|
+ command: 'node',
|
|
|
+ args: ['test.js'],
|
|
|
+ alwaysAllow: ['allowed-tool']
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }))
|
|
|
+
|
|
|
+ mcpHub = new McpHub(mockProvider as ClineProvider)
|
|
|
+ })
|
|
|
+
|
|
|
+ describe('toggleToolAlwaysAllow', () => {
|
|
|
+ it('should add tool to always allow list when enabling', async () => {
|
|
|
+ const mockConfig = {
|
|
|
+ mcpServers: {
|
|
|
+ 'test-server': {
|
|
|
+ command: 'node',
|
|
|
+ args: ['test.js'],
|
|
|
+ alwaysAllow: []
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Mock reading initial config
|
|
|
+ ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig))
|
|
|
+
|
|
|
+ await mcpHub.toggleToolAlwaysAllow('test-server', 'new-tool', true)
|
|
|
+
|
|
|
+ // Verify the config was updated correctly
|
|
|
+ const writeCall = (fs.writeFile as jest.Mock).mock.calls[0]
|
|
|
+ const writtenConfig = JSON.parse(writeCall[1])
|
|
|
+ expect(writtenConfig.mcpServers['test-server'].alwaysAllow).toContain('new-tool')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should remove tool from always allow list when disabling', async () => {
|
|
|
+ const mockConfig = {
|
|
|
+ mcpServers: {
|
|
|
+ 'test-server': {
|
|
|
+ command: 'node',
|
|
|
+ args: ['test.js'],
|
|
|
+ alwaysAllow: ['existing-tool']
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Mock reading initial config
|
|
|
+ ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig))
|
|
|
+
|
|
|
+ await mcpHub.toggleToolAlwaysAllow('test-server', 'existing-tool', false)
|
|
|
+
|
|
|
+ // Verify the config was updated correctly
|
|
|
+ const writeCall = (fs.writeFile as jest.Mock).mock.calls[0]
|
|
|
+ const writtenConfig = JSON.parse(writeCall[1])
|
|
|
+ expect(writtenConfig.mcpServers['test-server'].alwaysAllow).not.toContain('existing-tool')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should initialize alwaysAllow if it does not exist', async () => {
|
|
|
+ const mockConfig = {
|
|
|
+ mcpServers: {
|
|
|
+ 'test-server': {
|
|
|
+ command: 'node',
|
|
|
+ args: ['test.js']
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Mock reading initial config
|
|
|
+ ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig))
|
|
|
+
|
|
|
+ await mcpHub.toggleToolAlwaysAllow('test-server', 'new-tool', true)
|
|
|
+
|
|
|
+ // Verify the config was updated with initialized alwaysAllow
|
|
|
+ const writeCall = (fs.writeFile as jest.Mock).mock.calls[0]
|
|
|
+ const writtenConfig = JSON.parse(writeCall[1])
|
|
|
+ expect(writtenConfig.mcpServers['test-server'].alwaysAllow).toBeDefined()
|
|
|
+ expect(writtenConfig.mcpServers['test-server'].alwaysAllow).toContain('new-tool')
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ describe('callTool', () => {
|
|
|
+ it('should execute tool successfully', async () => {
|
|
|
+ // Mock the connection with a minimal client implementation
|
|
|
+ const mockConnection: McpConnection = {
|
|
|
+ server: {
|
|
|
+ name: 'test-server',
|
|
|
+ config: JSON.stringify({}),
|
|
|
+ status: 'connected' as const
|
|
|
+ },
|
|
|
+ client: {
|
|
|
+ request: jest.fn().mockResolvedValue({ result: 'success' })
|
|
|
+ } as any,
|
|
|
+ transport: {
|
|
|
+ start: jest.fn(),
|
|
|
+ close: jest.fn(),
|
|
|
+ stderr: { on: jest.fn() }
|
|
|
+ } as any
|
|
|
+ }
|
|
|
+
|
|
|
+ mcpHub.connections = [mockConnection]
|
|
|
+
|
|
|
+ await mcpHub.callTool('test-server', 'some-tool', {})
|
|
|
+
|
|
|
+ // Verify the request was made with correct parameters
|
|
|
+ expect(mockConnection.client.request).toHaveBeenCalledWith(
|
|
|
+ {
|
|
|
+ method: 'tools/call',
|
|
|
+ params: {
|
|
|
+ name: 'some-tool',
|
|
|
+ arguments: {}
|
|
|
+ }
|
|
|
+ },
|
|
|
+ expect.any(Object)
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should throw error if server not found', async () => {
|
|
|
+ await expect(mcpHub.callTool('non-existent-server', 'some-tool', {}))
|
|
|
+ .rejects
|
|
|
+ .toThrow('No connection found for server: non-existent-server')
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|