Skip to content

Commit dc1a4c3

Browse files
authored
feat: support mcp.json for MCP configurations (#4522)
* fix: text paste on chat input * fix: mcp server bootstrap * feat: support mcp.json * feat: support chinese tool name * chore: add comment for mcp tool name * feat: support load mcp server from mcp.json * feat: support mcp configuration editor title * chore: fix logic * chore: fix types * fix: save mcp server * chore: fix json schema * fix: mcp server start * chore: update test * fix: restart server after edit * fix: mcp configuration schema * chore: fix mcp * chore: fix remove * chore: fix test * chore: fix test
1 parent e0054bf commit dc1a4c3

27 files changed

Lines changed: 779 additions & 225 deletions

packages/ai-native/__test__/node/mcp-server.sse.test.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2-
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
32

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

@@ -37,7 +36,7 @@ describe('SSEMCPServer', () => {
3736
describe('constructor', () => {
3837
it('should initialize with correct parameters', () => {
3938
expect(server.getServerName()).toBe('test-server');
40-
expect(server.serverHost).toBe('http://localhost:3000');
39+
expect(server.url).toBe('http://localhost:3000');
4140
expect(server.isStarted()).toBe(false);
4241
});
4342
});
@@ -98,7 +97,16 @@ describe('SSEMCPServer', () => {
9897
describe('getTools', () => {
9998
const mockClient = {
10099
connect: jest.fn(),
101-
listTools: jest.fn().mockResolvedValue(['tool1', 'tool2']),
100+
listTools: jest.fn().mockResolvedValue({
101+
tools: [
102+
{
103+
name: 'tool1',
104+
},
105+
{
106+
name: 'tool2',
107+
},
108+
],
109+
}),
102110
onerror: jest.fn(),
103111
};
104112

@@ -110,7 +118,9 @@ describe('SSEMCPServer', () => {
110118
it('should return list of available tools', async () => {
111119
const tools = await server.getTools();
112120
expect(mockClient.listTools).toHaveBeenCalled();
113-
expect(tools).toEqual(['tool1', 'tool2']);
121+
expect(tools).toEqual({
122+
tools: [{ name: 'tool1' }, { name: 'tool2' }],
123+
});
114124
});
115125
});
116126

@@ -142,9 +152,9 @@ describe('SSEMCPServer', () => {
142152

143153
describe('update', () => {
144154
it('should update server configuration', () => {
145-
const newServerHost = 'http://localhost:4000';
146-
server.update(newServerHost);
147-
expect(server.serverHost).toBe(newServerHost);
155+
const newUrl = 'http://localhost:4000';
156+
server.update(newUrl);
157+
expect(server.url).toBe(newUrl);
148158
});
149159
});
150160
});

packages/ai-native/src/browser/ai-core.contribution.ts

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,13 @@ import {
5252
CommandService,
5353
InlineChatFeatureRegistryToken,
5454
IntelligentCompletionsRegistryToken,
55+
MCPConfigServiceToken,
56+
PreferenceScope,
5557
ProblemFixRegistryToken,
5658
RenameCandidatesProviderRegistryToken,
5759
ResolveConflictRegistryToken,
60+
STORAGE_NAMESPACE,
61+
StorageProvider,
5862
TerminalRegistryToken,
5963
isUndefined,
6064
runWhenIdle,
@@ -88,7 +92,7 @@ import {
8892
deepSeekModels,
8993
openAiNativeModels,
9094
} from '../common';
91-
import { MCPServerDescription } from '../common/mcp-server-manager';
95+
import { MCPServerDescription, MCPServersEnabledKey } from '../common/mcp-server-manager';
9296
import { MCP_SERVER_TYPE } from '../common/types';
9397

9498
import { ChatManagerService } from './chat/chat-manager.service';
@@ -111,6 +115,7 @@ import {
111115
} from './layout/tabbar.view';
112116
import { AIChatLogoAvatar } from './layout/view/avatar/avatar.view';
113117
import { BaseApplyService } from './mcp/base-apply.service';
118+
import { MCPConfigService } from './mcp/config/mcp-config.service';
114119
import {
115120
AINativeCoreContribution,
116121
IChatFeatureRegistry,
@@ -248,6 +253,9 @@ export class AINativeBrowserContribution
248253
@Autowired(SumiMCPServerProxyServicePath)
249254
private readonly sumiMCPServerBackendProxy: ISumiMCPServerBackend;
250255

256+
@Autowired(MCPConfigServiceToken)
257+
private readonly mcpConfigService: MCPConfigService;
258+
251259
@Autowired(WorkbenchEditorService)
252260
private readonly workbenchEditorService: WorkbenchEditorServiceImpl;
253261

@@ -260,6 +268,9 @@ export class AINativeBrowserContribution
260268
@Autowired(BaseApplyService)
261269
private readonly applyService: BaseApplyService;
262270

271+
@Autowired(StorageProvider)
272+
private readonly storageProvider: StorageProvider;
273+
263274
constructor() {
264275
this.registerFeature();
265276
}
@@ -411,25 +422,64 @@ export class AINativeBrowserContribution
411422
});
412423
}
413424

414-
private initMCPServers() {
415-
// 从 preferences 获取并初始化外部 MCP Servers
416-
const mcpServers = this.preferenceService.getValid<MCPServerDescription[]>(AINativeSettingSectionsId.MCPServers);
425+
private async initMCPServers() {
426+
const storage = await this.storageProvider(STORAGE_NAMESPACE.CHAT);
427+
let enabledMCPServers = storage.get<string[]>(MCPServersEnabledKey, [BUILTIN_MCP_SERVER_NAME]);
417428

418-
// 查找内置 MCP Server 的配置
419-
const builtinServer = mcpServers?.find(
420-
(server) => server.name === BUILTIN_MCP_SERVER_NAME && server.type === MCP_SERVER_TYPE.BUILTIN,
429+
const oldMCPServers = this.preferenceService.get<MCPServerDescription[]>(AINativeSettingSectionsId.MCPServers, []);
430+
let mcpServerFromWorkspace = this.preferenceService.resolve<{ mcpServers: Record<string, any> }>(
431+
'mcp',
432+
{
433+
mcpServers: {},
434+
},
435+
undefined,
421436
);
422-
437+
if (mcpServerFromWorkspace.scope === PreferenceScope.Default && oldMCPServers.length > 0) {
438+
// 如果用户没有配置,也没有存储,则从旧配置迁移
439+
const newMCPServers = {
440+
mcpServers: {},
441+
};
442+
const mcpServersEnabled = new Set<string>([BUILTIN_MCP_SERVER_NAME]);
443+
oldMCPServers.forEach((server) => {
444+
if (server.type === MCP_SERVER_TYPE.SSE) {
445+
newMCPServers.mcpServers[server.name] = {
446+
url: (server as any).serverHost,
447+
};
448+
} else if (server.type === MCP_SERVER_TYPE.STDIO) {
449+
newMCPServers.mcpServers[server.name] = {
450+
command: server.command,
451+
args: server.args,
452+
env: server.env,
453+
};
454+
}
455+
if (server.enabled) {
456+
mcpServersEnabled.add(server.name);
457+
}
458+
});
459+
await this.preferenceService.set('mcp', newMCPServers, PreferenceScope.Workspace);
460+
mcpServerFromWorkspace = this.preferenceService.resolve<{ mcpServers: Record<string, any> }>(
461+
'mcp',
462+
{
463+
mcpServers: {},
464+
},
465+
undefined,
466+
);
467+
enabledMCPServers = Array.from(mcpServersEnabled);
468+
storage.set(MCPServersEnabledKey, enabledMCPServers);
469+
}
470+
const userServers = mcpServerFromWorkspace.value?.mcpServers;
423471
// 总是初始化内置服务器,根据配置决定是否启用
424-
this.sumiMCPServerBackendProxy.$initBuiltinMCPServer(builtinServer?.enabled ?? true);
425-
426-
// 初始化其他外部 MCP Servers
427-
if (mcpServers && mcpServers.length > 0) {
428-
const externalServers = mcpServers.filter((server) => server.name !== BUILTIN_MCP_SERVER_NAME);
429-
if (externalServers.length > 0) {
430-
this.sumiMCPServerBackendProxy.$initExternalMCPServers(externalServers);
431-
}
472+
this.sumiMCPServerBackendProxy.$initBuiltinMCPServer(enabledMCPServers.includes(BUILTIN_MCP_SERVER_NAME));
473+
474+
if (userServers && Object.keys(userServers).length > 0) {
475+
const mcpServers = (
476+
await Promise.all(
477+
Object.keys(userServers).map(async (name) => await this.mcpConfigService.getServerConfigByName(name)),
478+
)
479+
).filter((server) => server !== undefined) as MCPServerDescription[];
480+
await this.sumiMCPServerBackendProxy.$initExternalMCPServers(mcpServers);
432481
}
482+
this.mcpConfigService.fireMCPServersChange(true);
433483
}
434484

435485
private getModelByName(modelName: string) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { ChatSlashCommandItemModel } from '../chat/chat-model';
2828
import { ChatProxyService } from '../chat/chat-proxy.service';
2929
import { ChatFeatureRegistry } from '../chat/chat.feature.registry';
3030
import { ChatInternalService } from '../chat/chat.internal.service';
31-
import { OPEN_MCP_CONFIG_COMMAND } from '../mcp/config/mcp-config.commands';
31+
import { MCPConfigCommands } from '../mcp/config/mcp-config.commands';
3232
import { MCPServerProxyService } from '../mcp/mcp-server-proxy.service';
3333
import { MCPToolsDialog } from '../mcp/mcp-tools-dialog.view';
3434
import { IChatSlashCommandItem } from '../types';
@@ -223,7 +223,7 @@ export const ChatInput = React.forwardRef((props: IChatInputProps, ref) => {
223223
const currentAgentIdRef = useLatest(agentId);
224224

225225
const handleShowMCPConfig = React.useCallback(() => {
226-
commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
226+
commandService.executeCommand(MCPConfigCommands.OPEN_MCP_CONFIG.id);
227227
}, [commandService]);
228228

229229
const handleShowMCPTools = React.useCallback(async () => {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { IChatInternalService } from '../../common';
1818
import { LLMContextService } from '../../common/llm-context';
1919
import { ChatFeatureRegistry } from '../chat/chat.feature.registry';
2020
import { ChatInternalService } from '../chat/chat.internal.service';
21-
import { OPEN_MCP_CONFIG_COMMAND } from '../mcp/config/mcp-config.commands';
21+
import { MCPConfigCommands } from '../mcp/config/mcp-config.commands';
2222

2323
import styles from './components.module.less';
2424
import { MentionInput } from './mention-input/mention-input';
@@ -71,7 +71,7 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
7171
const outlineTreeService = useInjectable<OutlineTreeService>(OutlineTreeService);
7272
const prevOutlineItems = useRef<MentionItem[]>([]);
7373
const handleShowMCPConfig = React.useCallback(() => {
74-
commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
74+
commandService.executeCommand(MCPConfigCommands.OPEN_MCP_CONFIG.id);
7575
}, [commandService]);
7676

7777
useEffect(() => {

packages/ai-native/src/browser/components/mention-input/mention-input.tsx

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
483483
// 这里可以添加额外的逻辑,如果需要的话
484484
};
485485

486-
// 添加粘贴事件处理
487486
const handlePaste = async (e: React.ClipboardEvent<HTMLDivElement>) => {
488487
const items = e.clipboardData.items;
489488

@@ -551,13 +550,16 @@ export const MentionInput: React.FC<MentionInputProps> = ({
551550
});
552551

553552
// 插入处理后的内容
553+
const lastNode = fragment.lastChild;
554554
range.insertNode(fragment);
555555

556556
// 将光标移动到插入内容的末尾
557-
range.setStartAfter(fragment);
558-
range.setEndAfter(fragment);
559-
selection.removeAllRanges();
560-
selection.addRange(range);
557+
if (lastNode && lastNode.parentNode) {
558+
const newRange = document.createRange();
559+
newRange.setStartAfter(lastNode);
560+
selection.removeAllRanges();
561+
selection.addRange(newRange);
562+
}
561563

562564
// 触发 input 事件以更新状态
563565
handleInput();
@@ -1060,25 +1062,27 @@ export const MentionInput: React.FC<MentionInputProps> = ({
10601062
</div>
10611063
<div className={styles.right_control}>
10621064
{renderButtons(FooterButtonPosition.RIGHT)}
1063-
<Popover
1064-
overlayClassName={styles.popover_icon}
1065-
id={'ai-chat-clear-context'}
1066-
position={PopoverPosition.top}
1067-
content={localize('aiNative.chat.context.clear')}
1068-
>
1069-
<div className={styles.context_container} onClick={handleClearContext}>
1070-
<div className={styles.context_icon}>
1071-
<Icon icon='out-link' />
1072-
<Icon icon='close' />
1073-
</div>
1074-
<div className={styles.context_description}>
1075-
{formatLocalize(
1076-
'aiNative.chat.context.description',
1077-
attachedFiles.files.length + attachedFiles.folders.length,
1078-
)}
1065+
{hasContext && (
1066+
<Popover
1067+
overlayClassName={styles.popover_icon}
1068+
id={'ai-chat-clear-context'}
1069+
position={PopoverPosition.top}
1070+
content={localize('aiNative.chat.context.clear')}
1071+
>
1072+
<div className={styles.context_container} onClick={handleClearContext}>
1073+
<div className={styles.context_icon}>
1074+
<Icon icon='out-link' />
1075+
<Icon icon='close' />
1076+
</div>
1077+
<div className={styles.context_description}>
1078+
{formatLocalize(
1079+
'aiNative.chat.context.description',
1080+
attachedFiles.files.length + attachedFiles.folders.length,
1081+
)}
1082+
</div>
10791083
</div>
1080-
</div>
1081-
</Popover>
1084+
</Popover>
1085+
)}
10821086
<Popover
10831087
overlayClassName={styles.popover_icon}
10841088
id={'ai-chat-send'}

packages/ai-native/src/browser/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import {
1515
} from '@opensumi/ide-core-browser';
1616
import {
1717
IntelligentCompletionsRegistryToken,
18+
MCPConfigServiceToken,
1819
ProblemFixRegistryToken,
1920
TerminalRegistryToken,
2021
} from '@opensumi/ide-core-common';
22+
import { FolderFilePreferenceProvider } from '@opensumi/ide-preferences/lib/browser/folder-file-preference-provider';
2123

2224
import {
2325
ChatProxyServiceToken,
@@ -60,6 +62,9 @@ import { LanguageParserService } from './languages/service';
6062
import { BaseApplyService } from './mcp/base-apply.service';
6163
import { MCPConfigCommandContribution } from './mcp/config/mcp-config.commands';
6264
import { MCPConfigContribution } from './mcp/config/mcp-config.contribution';
65+
import { MCPConfigService } from './mcp/config/mcp-config.service';
66+
import { MCPFolderPreferenceProvider } from './mcp/mcp-folder-preference-provider';
67+
import { MCPPreferencesContribution } from './mcp/mcp-preferences-contribution';
6368
import { MCPServerProxyService } from './mcp/mcp-server-proxy.service';
6469
import { MCPServerRegistry } from './mcp/mcp-server.feature.registry';
6570
import { CreateNewFileWithTextTool } from './mcp/tools/createNewFileWithText';
@@ -98,6 +103,7 @@ export class AINativeModule extends BrowserModule {
98103
IntelligentCompletionsContribution,
99104
MCPConfigContribution,
100105
MCPConfigCommandContribution,
106+
MCPPreferencesContribution,
101107

102108
// MCP Server Contributions START
103109
ListDirTool,
@@ -206,6 +212,16 @@ export class AINativeModule extends BrowserModule {
206212
token: BaseApplyService,
207213
useClass: ApplyService,
208214
},
215+
{
216+
token: MCPConfigServiceToken,
217+
useClass: MCPConfigService,
218+
},
219+
{
220+
token: FolderFilePreferenceProvider,
221+
useClass: MCPFolderPreferenceProvider,
222+
dropdownForTag: true,
223+
tag: 'mcp',
224+
},
209225
];
210226

211227
backServices = [

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,17 @@
2222
font-size: 14px;
2323
}
2424

25-
.addButton {
25+
.actionButton {
2626
padding: 8px 16px;
2727
border-radius: 4px;
2828
background-color: var(--button-primary-background);
2929
color: var(--button-primary-foreground);
3030
border: none;
3131
cursor: pointer;
3232
font-size: 13px;
33+
.actionButtonIcon {
34+
margin-right: 5px;
35+
}
3336

3437
&:hover {
3538
background-color: var(--button-primary-hover-background);

0 commit comments

Comments
 (0)