Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions packages/ai-native/__test__/common/mcp-server-manager.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';

import { MCPServerDescription, MCPServerManager } from '../../src/common/mcp-server-manager';
import { MCP_SERVER_TYPE } from '../../src/common/types';

describe('MCPServerManager Interface', () => {
let mockManager: MCPServerManager;
const mockClient = {
callTool: jest.fn(),
listTools: jest.fn(),
};

const mockServer: MCPServerDescription = {
name: 'test-server',
command: 'test-command',
args: ['arg1', 'arg2'],
env: { TEST_ENV: 'value' },
type: MCP_SERVER_TYPE.STDIO,
};

beforeEach(() => {
Expand Down Expand Up @@ -116,6 +112,7 @@ describe('MCPServerManager Interface', () => {
command: 'external-command',
args: ['ext-arg'],
env: { EXT_ENV: 'value' },
type: MCP_SERVER_TYPE.STDIO,
},
];

Expand Down
146 changes: 146 additions & 0 deletions packages/ai-native/__test__/node/mcp-server.sse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';

import { ILogger } from '@opensumi/ide-core-common';

import { SSEMCPServer } from '../../src/node/mcp-server.sse';

jest.mock('@modelcontextprotocol/sdk/client/index.js');
jest.mock('@modelcontextprotocol/sdk/client/sse.js');

describe('SSEMCPServer', () => {
let server: SSEMCPServer;
const mockLogger: ILogger = {
log: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
verbose: jest.fn(),
warn: jest.fn(),
critical: jest.fn(),
dispose: jest.fn(),
getLevel: jest.fn(),
setLevel: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
server = new SSEMCPServer('test-server', 'http://localhost:3000', mockLogger);
});

describe('constructor', () => {
it('should initialize with correct parameters', () => {
expect(server.getServerName()).toBe('test-server');
expect(server.serverHost).toBe('http://localhost:3000');
expect(server.isStarted()).toBe(false);
});
});

describe('start', () => {
beforeEach(() => {
(Client as jest.Mock).mockImplementation(() => ({
connect: jest.fn().mockResolvedValue(undefined),
onerror: jest.fn(),
}));
(SSEClientTransport as jest.Mock).mockImplementation(() => ({
onerror: jest.fn(),
}));
});

it('should start the server successfully', async () => {
await server.start();
expect(server.isStarted()).toBe(true);
expect(SSEClientTransport).toHaveBeenCalledWith(expect.any(URL));
});

it('should not start server if already started', async () => {
await server.start();
const firstCallCount = (SSEClientTransport as jest.Mock).mock.calls.length;
await server.start();
expect((SSEClientTransport as jest.Mock).mock.calls.length).toBe(firstCallCount);
});
});

describe('callTool', () => {
const mockClient = {
connect: jest.fn(),
callTool: jest.fn(),
onerror: jest.fn(),
};

beforeEach(async () => {
(Client as jest.Mock).mockImplementation(() => mockClient);
await server.start();
});

it('should call tool with parsed arguments', async () => {
const toolName = 'test-tool';
const argString = '{"key": "value"}';
await server.callTool(toolName, 'toolCallId', argString);
expect(mockClient.callTool).toHaveBeenCalledWith({
name: toolName,
toolCallId: 'toolCallId',
arguments: { key: 'value' },
});
});

it('should handle invalid JSON arguments', async () => {
const toolName = 'test-tool';
const invalidArgString = '{invalid json}';
await server.callTool(toolName, 'toolCallId', invalidArgString);
expect(mockLogger.error).toHaveBeenCalled();
});
});

describe('getTools', () => {
const mockClient = {
connect: jest.fn(),
listTools: jest.fn().mockResolvedValue(['tool1', 'tool2']),
onerror: jest.fn(),
};

beforeEach(async () => {
(Client as jest.Mock).mockImplementation(() => mockClient);
await server.start();
});

it('should return list of available tools', async () => {
const tools = await server.getTools();
expect(mockClient.listTools).toHaveBeenCalled();
expect(tools).toEqual(['tool1', 'tool2']);
});
});

describe('stop', () => {
const mockClient = {
connect: jest.fn(),
close: jest.fn(),
onerror: jest.fn(),
};

beforeEach(async () => {
(Client as jest.Mock).mockImplementation(() => mockClient);
await server.start();
});

it('should stop the server successfully', async () => {
await server.stop();
expect(mockClient.close).toHaveBeenCalled();
expect(server.isStarted()).toBe(false);
});

it('should not attempt to stop if server is not started', async () => {
await server.stop(); // First stop
mockClient.close.mockClear();
await server.stop(); // Second stop
expect(mockClient.close).not.toHaveBeenCalled();
});
});

describe('update', () => {
it('should update server configuration', () => {
const newServerHost = 'http://localhost:4000';
server.update(newServerHost);
expect(server.serverHost).toBe(newServerHost);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'

import { ILogger } from '@opensumi/ide-core-common';

import { StdioMCPServerImpl } from '../../src/node/mcp-server';
import { StdioMCPServer } from '../../src/node/mcp-server.stdio';

jest.mock('@modelcontextprotocol/sdk/client/index.js');
jest.mock('@modelcontextprotocol/sdk/client/stdio.js');

describe('StdioMCPServerImpl', () => {
let server: StdioMCPServerImpl;
describe('StdioMCPServer', () => {
let server: StdioMCPServer;
const mockLogger: ILogger = {
log: jest.fn(),
error: jest.fn(),
Expand All @@ -24,7 +24,7 @@ describe('StdioMCPServerImpl', () => {

beforeEach(() => {
jest.clearAllMocks();
server = new StdioMCPServerImpl('test-server', 'test-command', ['arg1', 'arg2'], { ENV: 'test' }, mockLogger);
server = new StdioMCPServer('test-server', 'test-command', ['arg1', 'arg2'], { ENV: 'test' }, mockLogger);
});

describe('constructor', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/ai-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"ansi-regex": "^2.0.0",
"diff": "^7.0.0",
"dom-align": "^1.7.0",
"eventsource": "^3.0.5",
"rc-collapse": "^4.0.0",
"react-chat-elements": "^12.0.10",
"react-highlight": "^0.15.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/ai-native/src/browser/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ export const ChatInput = React.forwardRef((props: IChatInputProps, ref) => {
>
<EnhanceIcon
wrapperClassName={styles.action_btn}
className={'codicon codicon-server'}
className={'codicon codicon-radio-tower'}
onClick={handleShowMCPConfig}
tabIndex={0}
role='button'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
align-items: center;
}

.typeTag {
margin-left: 7px;
}

.serverType {
background-color: var(--editor-background);
color: var(--descriptionForeground);
Expand Down Expand Up @@ -162,6 +166,15 @@
align-items: center;
gap: 8px;
flex-wrap: wrap;
&.link {
cursor: pointer;
opacity: 0.6;
transition: all 0.2s ease;
&:hover {
opacity: 1;
text-decoration: underline;
}
}
}

.toolTag {
Expand Down
Loading