Skip to content

Commit 105122b

Browse files
authored
feat: support SSE MCP Server and improve form style (#4447)
* feat: support SSE MCP Server * feat: improve types defined * fix: typos * chore: add i18n text * feat: support node14 and improve MCP Server configuration page * style: improve the style of the server type * chore: update package version * feat: support configs update and vlidate form * fix: test * chore: fix test
1 parent 2c14b6e commit 105122b

23 files changed

Lines changed: 781 additions & 218 deletions

packages/ai-native/__test__/common/mcp-server-manager.test.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
1-
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2-
31
import { MCPServerDescription, MCPServerManager } from '../../src/common/mcp-server-manager';
2+
import { MCP_SERVER_TYPE } from '../../src/common/types';
43

54
describe('MCPServerManager Interface', () => {
65
let mockManager: MCPServerManager;
7-
const mockClient = {
8-
callTool: jest.fn(),
9-
listTools: jest.fn(),
10-
};
116

127
const mockServer: MCPServerDescription = {
138
name: 'test-server',
149
command: 'test-command',
1510
args: ['arg1', 'arg2'],
1611
env: { TEST_ENV: 'value' },
12+
type: MCP_SERVER_TYPE.STDIO,
1713
};
1814

1915
beforeEach(() => {
@@ -116,6 +112,7 @@ describe('MCPServerManager Interface', () => {
116112
command: 'external-command',
117113
args: ['ext-arg'],
118114
env: { EXT_ENV: 'value' },
115+
type: MCP_SERVER_TYPE.STDIO,
119116
},
120117
];
121118

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
3+
4+
import { ILogger } from '@opensumi/ide-core-common';
5+
6+
import { SSEMCPServer } from '../../src/node/mcp-server.sse';
7+
8+
jest.mock('@modelcontextprotocol/sdk/client/index.js');
9+
jest.mock('@modelcontextprotocol/sdk/client/sse.js');
10+
11+
describe('SSEMCPServer', () => {
12+
let server: SSEMCPServer;
13+
const mockLogger: ILogger = {
14+
log: jest.fn(),
15+
error: jest.fn(),
16+
debug: jest.fn(),
17+
verbose: jest.fn(),
18+
warn: jest.fn(),
19+
critical: jest.fn(),
20+
dispose: jest.fn(),
21+
getLevel: jest.fn(),
22+
setLevel: jest.fn(),
23+
};
24+
25+
beforeEach(() => {
26+
jest.clearAllMocks();
27+
server = new SSEMCPServer('test-server', 'http://localhost:3000', mockLogger);
28+
});
29+
30+
describe('constructor', () => {
31+
it('should initialize with correct parameters', () => {
32+
expect(server.getServerName()).toBe('test-server');
33+
expect(server.serverHost).toBe('http://localhost:3000');
34+
expect(server.isStarted()).toBe(false);
35+
});
36+
});
37+
38+
describe('start', () => {
39+
beforeEach(() => {
40+
(Client as jest.Mock).mockImplementation(() => ({
41+
connect: jest.fn().mockResolvedValue(undefined),
42+
onerror: jest.fn(),
43+
}));
44+
(SSEClientTransport as jest.Mock).mockImplementation(() => ({
45+
onerror: jest.fn(),
46+
}));
47+
});
48+
49+
it('should start the server successfully', async () => {
50+
await server.start();
51+
expect(server.isStarted()).toBe(true);
52+
expect(SSEClientTransport).toHaveBeenCalledWith(expect.any(URL));
53+
});
54+
55+
it('should not start server if already started', async () => {
56+
await server.start();
57+
const firstCallCount = (SSEClientTransport as jest.Mock).mock.calls.length;
58+
await server.start();
59+
expect((SSEClientTransport as jest.Mock).mock.calls.length).toBe(firstCallCount);
60+
});
61+
});
62+
63+
describe('callTool', () => {
64+
const mockClient = {
65+
connect: jest.fn(),
66+
callTool: jest.fn(),
67+
onerror: jest.fn(),
68+
};
69+
70+
beforeEach(async () => {
71+
(Client as jest.Mock).mockImplementation(() => mockClient);
72+
await server.start();
73+
});
74+
75+
it('should call tool with parsed arguments', async () => {
76+
const toolName = 'test-tool';
77+
const argString = '{"key": "value"}';
78+
await server.callTool(toolName, 'toolCallId', argString);
79+
expect(mockClient.callTool).toHaveBeenCalledWith({
80+
name: toolName,
81+
toolCallId: 'toolCallId',
82+
arguments: { key: 'value' },
83+
});
84+
});
85+
86+
it('should handle invalid JSON arguments', async () => {
87+
const toolName = 'test-tool';
88+
const invalidArgString = '{invalid json}';
89+
await server.callTool(toolName, 'toolCallId', invalidArgString);
90+
expect(mockLogger.error).toHaveBeenCalled();
91+
});
92+
});
93+
94+
describe('getTools', () => {
95+
const mockClient = {
96+
connect: jest.fn(),
97+
listTools: jest.fn().mockResolvedValue(['tool1', 'tool2']),
98+
onerror: jest.fn(),
99+
};
100+
101+
beforeEach(async () => {
102+
(Client as jest.Mock).mockImplementation(() => mockClient);
103+
await server.start();
104+
});
105+
106+
it('should return list of available tools', async () => {
107+
const tools = await server.getTools();
108+
expect(mockClient.listTools).toHaveBeenCalled();
109+
expect(tools).toEqual(['tool1', 'tool2']);
110+
});
111+
});
112+
113+
describe('stop', () => {
114+
const mockClient = {
115+
connect: jest.fn(),
116+
close: jest.fn(),
117+
onerror: jest.fn(),
118+
};
119+
120+
beforeEach(async () => {
121+
(Client as jest.Mock).mockImplementation(() => mockClient);
122+
await server.start();
123+
});
124+
125+
it('should stop the server successfully', async () => {
126+
await server.stop();
127+
expect(mockClient.close).toHaveBeenCalled();
128+
expect(server.isStarted()).toBe(false);
129+
});
130+
131+
it('should not attempt to stop if server is not started', async () => {
132+
await server.stop(); // First stop
133+
mockClient.close.mockClear();
134+
await server.stop(); // Second stop
135+
expect(mockClient.close).not.toHaveBeenCalled();
136+
});
137+
});
138+
139+
describe('update', () => {
140+
it('should update server configuration', () => {
141+
const newServerHost = 'http://localhost:4000';
142+
server.update(newServerHost);
143+
expect(server.serverHost).toBe(newServerHost);
144+
});
145+
});
146+
});

packages/ai-native/__test__/node/mcp-server.test.ts renamed to packages/ai-native/__test__/node/mcp-server.stdio.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
33

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

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

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

11-
describe('StdioMCPServerImpl', () => {
12-
let server: StdioMCPServerImpl;
11+
describe('StdioMCPServer', () => {
12+
let server: StdioMCPServer;
1313
const mockLogger: ILogger = {
1414
log: jest.fn(),
1515
error: jest.fn(),
@@ -24,7 +24,7 @@ describe('StdioMCPServerImpl', () => {
2424

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

3030
describe('constructor', () => {

packages/ai-native/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"ansi-regex": "^2.0.0",
5050
"diff": "^7.0.0",
5151
"dom-align": "^1.7.0",
52+
"eventsource": "^3.0.5",
5253
"rc-collapse": "^4.0.0",
5354
"react-chat-elements": "^12.0.10",
5455
"react-highlight": "^0.15.0",

packages/ai-native/src/browser/components/ChatInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ export const ChatInput = React.forwardRef((props: IChatInputProps, ref) => {
512512
>
513513
<EnhanceIcon
514514
wrapperClassName={styles.action_btn}
515-
className={'codicon codicon-server'}
515+
className={'codicon codicon-radio-tower'}
516516
onClick={handleShowMCPConfig}
517517
tabIndex={0}
518518
role='button'

packages/ai-native/src/browser/mcp/config/components/mcp-config.module.less

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@
8585
align-items: center;
8686
}
8787

88+
.typeTag {
89+
margin-left: 7px;
90+
}
91+
8892
.serverType {
8993
background-color: var(--editor-background);
9094
color: var(--descriptionForeground);
@@ -162,6 +166,15 @@
162166
align-items: center;
163167
gap: 8px;
164168
flex-wrap: wrap;
169+
&.link {
170+
cursor: pointer;
171+
opacity: 0.6;
172+
transition: all 0.2s ease;
173+
&:hover {
174+
opacity: 1;
175+
text-decoration: underline;
176+
}
177+
}
165178
}
166179

167180
.toolTag {

0 commit comments

Comments
 (0)