Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support MCP server and client #4335

Merged
merged 40 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9bd7924
feat: mcp server client poc
life2015 Jan 21, 2025
65e12be
feat: introduce MCP tools contribution
life2015 Jan 21, 2025
b99e11d
fix: 修复 mcp sdk 引入类型问题
ensorrow Jan 22, 2025
9520f4c
feat: add builtin MCP server
life2015 Jan 22, 2025
c54b20f
fix: mcp types fix
life2015 Jan 22, 2025
83fc2b7
fix: mcp types fix2
life2015 Jan 22, 2025
bafda53
feat: sumi mcp builtin sever
life2015 Jan 23, 2025
301df94
feat: code optimization
life2015 Jan 24, 2025
6d94523
feat: support llm tool call streaming and ui, more mcp tools
life2015 Feb 7, 2025
5de2d5e
feat: enhance language model error handling and streaming
ensorrow Feb 10, 2025
96b3331
feat: mcp tools grouped by clientId, add mcp tools panel
life2015 Feb 10, 2025
c4e719c
feat: add openai compatible api preferences
ensorrow Feb 11, 2025
04e954a
feat: support chat history in language model request
life2015 Feb 11, 2025
fa713fd
feat: add MCP server configuration support via preferences
life2015 Feb 11, 2025
7703231
feat: implement readfile & readdir tools
ensorrow Feb 11, 2025
67c8b2b
fix: tool impl bugs
ensorrow Feb 11, 2025
384c73d
Merge branch 'feat/mcp-server-client-poc-2' of https://github.com/ope…
ensorrow Feb 11, 2025
d56fdce
refactor: use design system variables in ChatToolRender styles
life2015 Feb 11, 2025
e00b72f
refactor: improve logging and revert some unnecessary optimization
life2015 Feb 12, 2025
15e612e
fix: logger not work in node.js
life2015 Feb 12, 2025
e120ad5
fix: mcp tool render fix
life2015 Feb 12, 2025
8aec9b2
feat: add MCP and custom LLM config
life2015 Feb 13, 2025
8ddda06
Merge branch 'main' into feat/mcp-server-client-poc-2
life2015 Feb 13, 2025
830a403
fix: build error fix
life2015 Feb 13, 2025
590818a
fix: lint fix
life2015 Feb 13, 2025
4752a83
fix: lint fix
life2015 Feb 13, 2025
027284e
fix: lint error fix
life2015 Feb 13, 2025
08374f2
feat: format the tool call error message
ensorrow Feb 14, 2025
3cf5bc5
feat: add doc, lint fix, create file tool fix
life2015 Feb 17, 2025
8eb3a83
Merge branch 'main' into feat/mcp-server-client-poc-2
life2015 Feb 17, 2025
533c1af
fix: lint fix
life2015 Feb 17, 2025
4777ed7
feat: add llmcontext service (#4374)
Aaaaash Feb 17, 2025
0fd9e82
feat: update chat agent prompt provider to use serialized context
Aaaaash Feb 17, 2025
328dadf
fix: build error
Aaaaash Feb 17, 2025
b236396
feat: close selector when click outside
Aaaaash Feb 17, 2025
4037afd
chore: import order
Aaaaash Feb 17, 2025
86efa1a
Merge branch 'main' into feat/mcp-server-client-poc-2
life2015 Feb 18, 2025
c942832
Merge branch 'main' into feat/mcp-server-client-poc-2
life2015 Feb 18, 2025
956b3db
feat: run terminal cmd tool (#4383)
Aaaaash Feb 18, 2025
50a4f78
feat: add some unit tests
life2015 Feb 18, 2025
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,5 @@ tools/workspace
# jupyter
.ipynb_checkpoints

*.tsbuildinfo
*.tsbuildinfo
.env
195 changes: 195 additions & 0 deletions packages/ai-native/__test__/node/mcp-server-manager-impl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { MCPServerManagerImpl } from '../../src/node/mcp-server-manager-impl';
import { MCPServerDescription } from '../../src/common/mcp-server-manager';
import { IMCPServer, MCPServerImpl } from '../../src/node/mcp-server';

jest.mock('../../src/node/mcp-server');

describe('MCPServerManagerImpl', () => {
let manager: MCPServerManagerImpl;
let mockServer: jest.Mocked<IMCPServer>;

beforeEach(() => {
jest.clearAllMocks();
manager = new MCPServerManagerImpl();
mockServer = {
isStarted: jest.fn(),
start: jest.fn(),
callTool: jest.fn(),
getTools: jest.fn(),
update: jest.fn(),
stop: jest.fn(),
};
jest.mocked(MCPServerImpl).mockImplementation(() => mockServer as unknown as MCPServerImpl);
});

describe('addOrUpdateServer', () => {
const serverDescription: MCPServerDescription = {
name: 'test-server',
command: 'test-command',
args: [],
env: {}
};

it('should add a new server', () => {
manager.addOrUpdateServer(serverDescription);
expect(MCPServerImpl).toHaveBeenCalledWith(
serverDescription.name,
serverDescription.command,
serverDescription.args,
serverDescription.env
);
});

it('should update existing server', () => {
manager.addOrUpdateServer(serverDescription);
const updatedDescription = { ...serverDescription, command: 'new-command' };
manager.addOrUpdateServer(updatedDescription);
expect(mockServer.update).toHaveBeenCalledWith(
updatedDescription.command,
updatedDescription.args,
updatedDescription.env
);
});
});

describe('startServer', () => {
it('should start an existing server', async () => {
manager.addOrUpdateServer({
name: 'test-server',
command: 'test-command',
args: [],
env: {}
});

await manager.startServer('test-server');
expect(mockServer.start).toHaveBeenCalled();
});

it('should throw error when starting non-existent server', async () => {
await expect(manager.startServer('non-existent')).rejects.toThrow(
'MCP server "non-existent" not found.'
);
});
});

describe('stopServer', () => {
it('should stop an existing server', async () => {
manager.addOrUpdateServer({
name: 'test-server',
command: 'test-command',
args: [],
env: {}
});

await manager.stopServer('test-server');
expect(mockServer.stop).toHaveBeenCalled();
});

it('should throw error when stopping non-existent server', async () => {
await expect(manager.stopServer('non-existent')).rejects.toThrow(
'MCP server "non-existent" not found.'
);
});
});

describe('getStartedServers', () => {
it('should return list of started servers', async () => {
manager.addOrUpdateServer({
name: 'server1',
command: 'cmd1',
args: [],
env: {}
});
manager.addOrUpdateServer({
name: 'server2',
command: 'cmd2',
args: [],
env: {}
});

mockServer.isStarted.mockReturnValueOnce(true).mockReturnValueOnce(false);
const startedServers = await manager.getStartedServers();
expect(startedServers).toEqual(['server1']);
});
});

describe('getServerNames', () => {
it('should return list of all server names', async () => {
manager.addOrUpdateServer({
name: 'server1',
command: 'cmd1',
args: [],
env: {}
});
manager.addOrUpdateServer({
name: 'server2',
command: 'cmd2',
args: [],
env: {}
});

const serverNames = await manager.getServerNames();
expect(serverNames).toEqual(['server1', 'server2']);
});
});

describe('removeServer', () => {
it('should remove an existing server', () => {
manager.addOrUpdateServer({
name: 'test-server',
command: 'test-command',
args: [],
env: {}
});

manager.removeServer('test-server');
expect(mockServer.stop).toHaveBeenCalled();
});

it('should handle removing non-existent server', () => {
const consoleSpy = jest.spyOn(console, 'warn');
manager.removeServer('non-existent');
expect(consoleSpy).toHaveBeenCalledWith('MCP server "non-existent" not found.');
});
});

describe('callTool', () => {
it('should call tool on existing server', () => {
manager.addOrUpdateServer({
name: 'test-server',
command: 'test-command',
args: [],
env: {}
});

manager.callTool('test-server', 'test-tool', 'test-args');
expect(mockServer.callTool).toHaveBeenCalledWith('test-tool', 'test-args');
});

it('should throw error when calling tool on non-existent server', () => {
expect(() => manager.callTool('non-existent', 'test-tool', 'test-args')).toThrow(
'MCP server "test-tool" not found.'
);
});
});

describe('getTools', () => {
it('should get tools from existing server', async () => {
manager.addOrUpdateServer({
name: 'test-server',
command: 'test-command',
args: [],
env: {}
});

await manager.getTools('test-server');
expect(mockServer.getTools).toHaveBeenCalled();
});

it('should throw error when getting tools from non-existent server', async () => {
await expect(manager.getTools('non-existent')).rejects.toThrow(
'MCP server "non-existent" not found.'
);
});
});
});
6 changes: 6 additions & 0 deletions packages/ai-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"url": "[email protected]:opensumi/core.git"
},
"dependencies": {
"@ai-sdk/anthropic": "^1.1.6",
"@ai-sdk/deepseek": "^0.1.8",
"@anthropic-ai/sdk": "^0.36.3",
"@modelcontextprotocol/sdk": "^1.3.1",
"@opensumi/ide-components": "workspace:*",
"@opensumi/ide-core-common": "workspace:*",
"@opensumi/ide-core-node": "workspace:*",
Expand All @@ -38,8 +42,10 @@
"@opensumi/ide-utils": "workspace:*",
"@opensumi/ide-workspace": "workspace:*",
"@xterm/xterm": "5.5.0",
"ai": "^4.1.21",
"ansi-regex": "^2.0.0",
"dom-align": "^1.7.0",
"openai": "^4.55.7",
"react-chat-elements": "^12.0.10",
"react-highlight": "^0.15.0",
"tiktoken": "1.0.12",
Expand Down
62 changes: 62 additions & 0 deletions packages/ai-native/src/browser/ai-core.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ import {
AI_CHAT_VIEW_ID,
AI_MENU_BAR_DEBUG_TOOLBAR,
ChatProxyServiceToken,
ISumiMCPServerBackend,
SumiMCPServerProxyServicePath,
} from '../common';
import { MCPServerDescription, MCPServerManager, MCPServerManagerPath } from '../common/mcp-server-manager';
import { ToolInvocationRegistry, ToolInvocationRegistryImpl } from '../common/tool-invocation-registry';

import { ChatProxyService } from './chat/chat-proxy.service';
import { AIChatView } from './chat/chat.view';
Expand All @@ -94,10 +98,13 @@ import {
IChatFeatureRegistry,
IChatRenderRegistry,
IIntelligentCompletionsRegistry,
IMCPServerRegistry,
IProblemFixProviderRegistry,
IRenameCandidatesProviderRegistry,
IResolveConflictRegistry,
ITerminalProviderRegistry,
MCPServerContribution,
TokenMCPServerRegistry,
} from './types';
import { InlineChatEditorController } from './widget/inline-chat/inline-chat-editor.controller';
import { InlineChatFeatureRegistry } from './widget/inline-chat/inline-chat.feature.registry';
Expand Down Expand Up @@ -142,6 +149,12 @@ export class AINativeBrowserContribution
@Autowired(AINativeCoreContribution)
private readonly contributions: ContributionProvider<AINativeCoreContribution>;

@Autowired(MCPServerContribution)
private readonly mcpServerContributions: ContributionProvider<MCPServerContribution>;

@Autowired(TokenMCPServerRegistry)
private readonly mcpServerRegistry: IMCPServerRegistry;

@Autowired(InlineChatFeatureRegistryToken)
private readonly inlineChatFeatureRegistry: InlineChatFeatureRegistry;

Expand Down Expand Up @@ -205,6 +218,12 @@ export class AINativeBrowserContribution
@Autowired(CodeActionSingleHandler)
private readonly codeActionSingleHandler: CodeActionSingleHandler;

// @Autowired(MCPServerManagerPath)
// private readonly mcpServerManager: MCPServerManager;

@Autowired(SumiMCPServerProxyServicePath)
private readonly sumiMCPServerBackendProxy: ISumiMCPServerBackend;

constructor() {
this.registerFeature();
}
Expand Down Expand Up @@ -289,6 +308,8 @@ export class AINativeBrowserContribution
if (supportsInlineChat) {
this.codeActionSingleHandler.load();
}

this.sumiMCPServerBackendProxy.initBuiltinMCPServer();
});
}

Expand All @@ -303,6 +324,11 @@ export class AINativeBrowserContribution
contribution.registerIntelligentCompletionFeature?.(this.intelligentCompletionsRegistry);
contribution.registerProblemFixFeature?.(this.problemFixProviderRegistry);
});

// 注册 Opensumi 框架提供的 MCP Server Tools 能力 (此时的 Opensumi 作为 MCP Server)
this.mcpServerContributions.getContributions().forEach((contribution) => {
contribution.registerMCPServer(this.mcpServerRegistry);
});
}

registerSetting(registry: ISettingRegistry) {
Expand Down Expand Up @@ -367,6 +393,21 @@ export class AINativeBrowserContribution
},
],
});

// Register language model API key settings
registry.registerSettingSection(AI_NATIVE_SETTING_GROUP_ID, {
title: localize('preference.ai.native.apiKeys.title'),
preferences: [
{
id: AINativeSettingSectionsId.DeepseekApiKey,
localized: 'preference.ai.native.deepseek.apiKey',
},
{
id: AINativeSettingSectionsId.AnthropicApiKey,
localized: 'preference.ai.native.anthropic.apiKey',
},
],
});
}

if (this.aiNativeConfigService.capabilities.supportsInlineChat) {
Expand Down Expand Up @@ -407,6 +448,27 @@ export class AINativeBrowserContribution
}

registerCommands(commands: CommandRegistry): void {
commands.registerCommand(
{ id: 'ai.native.mcp.start', label: 'MCP: Start MCP Server' },
{
execute: async () => {

// TODO 支持第三方 MCP Server
const description: MCPServerDescription = {
name: 'filesystem',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '/Users/retrox/AlipayProjects/core'],
env: {},
};

// this.mcpServerManager.addOrUpdateServer(description);

// await this.mcpServerManager.startServer(description.name);
// await this.mcpServerManager.collectTools(description.name);
},
},
);

commands.registerCommand(AI_INLINE_CHAT_VISIBLE, {
execute: (value: boolean) => {
this.aiInlineChatService._onInlineChatVisible.fire(value);
Expand Down
Loading