Skip to content

Commit 1cbd37c

Browse files
erha19Aaaaash
andauthored
feat: improve MCP Server edit UX and i18n (#4455)
* feat: improve MCP Server edit UX and i18n * fix: correct typo in Chinese localization for full screen expansion * fix: remove duplicate messageService injection in MCPConfigView --------- Co-authored-by: 大表哥 <[email protected]> Co-authored-by: xubing.bxb <[email protected]>
1 parent 36f67c8 commit 1cbd37c

4 files changed

Lines changed: 97 additions & 66 deletions

File tree

packages/ai-native/src/browser/mcp/config/components/mcp-config.view.tsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import React, { useCallback } from 'react';
44
import { Badge } from '@opensumi/ide-components';
55
import { AINativeSettingSectionsId, ILogger, useInjectable } from '@opensumi/ide-core-browser';
66
import { PreferenceService } from '@opensumi/ide-core-browser/lib/preferences';
7-
import { localize } from '@opensumi/ide-core-common';
8-
import { IMessageService } from '@opensumi/ide-overlay/lib/common';
7+
import { PreferenceScope, localize } from '@opensumi/ide-core-common';
8+
import { IMessageService } from '@opensumi/ide-overlay';
99

1010
import { BUILTIN_MCP_SERVER_NAME, ISumiMCPServerBackend, SumiMCPServerProxyServicePath } from '../../../../common';
1111
import { MCPServerDescription } from '../../../../common/mcp-server-manager';
@@ -18,13 +18,13 @@ import { MCPServerForm, MCPServerFormData } from './mcp-server-form';
1818
export const MCPConfigView: React.FC = () => {
1919
const mcpServerProxyService = useInjectable<MCPServerProxyService>(MCPServerProxyService);
2020
const preferenceService = useInjectable<PreferenceService>(PreferenceService);
21+
const messageService = useInjectable<IMessageService>(IMessageService);
2122
const sumiMCPServerBackendProxy = useInjectable<ISumiMCPServerBackend>(SumiMCPServerProxyServicePath);
2223
const logger = useInjectable<ILogger>(ILogger);
23-
const messageService = useInjectable<IMessageService>(IMessageService);
2424
const [servers, setServers] = React.useState<MCPServer[]>([]);
2525
const [formVisible, setFormVisible] = React.useState(false);
2626
const [editingServer, setEditingServer] = React.useState<MCPServerFormData | undefined>();
27-
27+
const [loadingServer, setLoadingServer] = React.useState<string | undefined>();
2828
const loadServers = useCallback(async () => {
2929
const allServers = await mcpServerProxyService.$getServers();
3030
setServers(allServers);
@@ -44,6 +44,7 @@ export const MCPConfigView: React.FC = () => {
4444
const handleServerControl = useCallback(
4545
async (serverName: string, start: boolean) => {
4646
try {
47+
setLoadingServer(serverName);
4748
if (start) {
4849
await mcpServerProxyService.$startServer(serverName);
4950
} else {
@@ -87,12 +88,14 @@ export const MCPConfigView: React.FC = () => {
8788
});
8889
}
8990

90-
await preferenceService.set(AINativeSettingSectionsId.MCPServers, updatedServers);
91+
await preferenceService.set(AINativeSettingSectionsId.MCPServers, updatedServers, PreferenceScope.User);
9192
await loadServers();
93+
setLoadingServer(undefined);
9294
} catch (error) {
9395
const msg = error.message || error;
9496
logger.error(`Failed to ${start ? 'start' : 'stop'} server ${serverName}:`, error);
95-
messageService.error(`Failed to ${start ? 'start' : 'stop'} server ${serverName}:` + msg);
97+
messageService.error(error.message);
98+
setLoadingServer(undefined);
9699
}
97100
},
98101
[mcpServerProxyService, preferenceService, sumiMCPServerBackendProxy, loadServers],
@@ -121,7 +124,7 @@ export const MCPConfigView: React.FC = () => {
121124
const servers = preferenceService.get<MCPServerFormData[]>(AINativeSettingSectionsId.MCPServers, []);
122125
const updatedServers = servers.filter((s) => s.name !== serverName);
123126
sumiMCPServerBackendProxy.removeServer(serverName);
124-
await preferenceService.set(AINativeSettingSectionsId.MCPServers, updatedServers);
127+
await preferenceService.set(AINativeSettingSectionsId.MCPServers, updatedServers, PreferenceScope.User);
125128
await loadServers();
126129
},
127130
[editingServer, formVisible],
@@ -140,7 +143,7 @@ export const MCPConfigView: React.FC = () => {
140143
setServers(servers as MCPServer[]);
141144
setFormVisible(false);
142145
await sumiMCPServerBackendProxy.addOrUpdateServer(data as MCPServerDescription);
143-
await preferenceService.set(AINativeSettingSectionsId.MCPServers, servers);
146+
await preferenceService.set(AINativeSettingSectionsId.MCPServers, servers, PreferenceScope.User);
144147
await loadServers();
145148
},
146149
[servers, formVisible, loadServers],
@@ -164,10 +167,10 @@ export const MCPConfigView: React.FC = () => {
164167
<div className={styles.header}>
165168
<div>
166169
<h2 className={styles.title}>MCP Servers</h2>
167-
<p className={styles.description}>Manage your MCP server connections.</p>
170+
<p className={styles.description}>{localize('ai.native.mcp.manage.connections')}</p>
168171
</div>
169172
<button className={styles.addButton} onClick={handleAddServer}>
170-
+ Add new MCP server
173+
+ {localize('ai.native.mcp.addMCPServer.title')}
171174
</button>
172175
</div>
173176
<div className={styles.serversList}>
@@ -188,7 +191,15 @@ export const MCPConfigView: React.FC = () => {
188191
title={server.isStarted ? 'Stop' : 'Start'}
189192
onClick={() => handleServerControl(server.name, !server.isStarted)}
190193
>
191-
<i className={`codicon ${server.isStarted ? 'codicon-debug-stop' : 'codicon-debug-start'}`} />
194+
<i
195+
className={`codicon ${
196+
loadingServer === server.name
197+
? 'codicon-loading kt-icon-loading'
198+
: server.isStarted
199+
? 'codicon-debug-stop'
200+
: 'codicon-debug-start'
201+
}`}
202+
/>
192203
</button>
193204
{server.name !== BUILTIN_MCP_SERVER_NAME && (
194205
<button className={styles.iconButton} title='Delete' onClick={() => handleDeleteServer(server.name)}>
@@ -201,7 +212,7 @@ export const MCPConfigView: React.FC = () => {
201212
<div className={styles.detailRow}>
202213
<span className={styles.detailLabel}>Status:</span>
203214
<span className={`${styles.serverStatus} ${server.isStarted ? styles.running : styles.stopped}`}>
204-
{server.isStarted ? 'Running' : 'Stopped'}
215+
{server.isStarted ? localize('ai.native.mcp.running') : localize('ai.native.mcp.stopped')}
205216
</span>
206217
</div>
207218
{server.type && (

packages/ai-native/src/browser/mcp/config/components/mcp-server-form.tsx

Lines changed: 68 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -67,35 +67,75 @@ export const MCPServerForm: FC<Props> = ({ visible, initialData, onSave, onCance
6767
);
6868
}, [initialData]);
6969

70-
const handleSubmit = (e: FormEvent) => {
71-
e.preventDefault();
72-
const isValid = validateForm(formData);
73-
if (!isValid) {
74-
return;
75-
}
76-
const form = {
77-
...formData,
78-
};
79-
if (formData.type === MCP_SERVER_TYPE.SSE) {
80-
form.serverHost = form.serverHost?.trim();
81-
} else {
82-
const args = argsText.split(' ').filter(Boolean);
83-
const env = envText
84-
.split('\n')
85-
.filter(Boolean)
86-
.reduce((acc, line) => {
87-
const [key, value] = line.split('=');
88-
if (key && value) {
89-
acc[key.trim()] = value.trim();
90-
}
91-
return acc;
92-
}, {} as Record<string, string>);
93-
form.args = args;
94-
form.env = env;
95-
}
70+
const validateForm = useCallback(
71+
(formData: MCPServerFormData) => {
72+
if (formData.name.trim() === '') {
73+
messageService.error(localize('ai.native.mcp.name.isRequired'));
74+
return false;
75+
}
76+
if (
77+
!initialData &&
78+
existingServers.some((server) => server.name.toLocaleLowerCase() === formData.name.toLocaleLowerCase())
79+
) {
80+
messageService.error(formatLocalize('ai.native.mcp.serverNameExists', formData.name));
81+
return false;
82+
}
83+
if (formData.type === MCP_SERVER_TYPE.SSE) {
84+
const isServerHostValid = formData.serverHost?.trim() !== '';
85+
if (!isServerHostValid) {
86+
messageService.error(localize('ai.native.mcp.serverHost.isRequired'));
87+
}
88+
return isServerHostValid;
89+
}
90+
const isCommandValid = formData.command?.trim() !== '';
91+
if (!isCommandValid) {
92+
messageService.error(localize('ai.native.mcp.command.isRequired'));
93+
}
94+
return isCommandValid;
95+
},
96+
[existingServers, initialData],
97+
);
9698

97-
onSave(form);
98-
};
99+
const handleSubmit = useCallback(
100+
(e: FormEvent) => {
101+
e.preventDefault();
102+
const isValid = validateForm(formData);
103+
if (!isValid) {
104+
return;
105+
}
106+
const form = {
107+
...formData,
108+
};
109+
if (formData.type === MCP_SERVER_TYPE.SSE) {
110+
form.serverHost = form.serverHost?.trim();
111+
} else {
112+
const args = argsText.split(' ').filter(Boolean);
113+
const env = envText
114+
.split('\n')
115+
.filter(Boolean)
116+
.reduce((acc, line) => {
117+
const [key, value] = line.split('=');
118+
if (key && value) {
119+
acc[key.trim()] = value.trim();
120+
}
121+
return acc;
122+
}, {} as Record<string, string>);
123+
form.args = args;
124+
form.env = env;
125+
}
126+
127+
setFormData({
128+
...formData,
129+
command: '',
130+
serverHost: '',
131+
args: [],
132+
env: {},
133+
});
134+
135+
onSave(form);
136+
},
137+
[formData, argsText, envText, onSave, validateForm],
138+
);
99139

100140
const handleCommandChange = useCallback(
101141
(e: ChangeEvent<HTMLInputElement>) => {
@@ -183,32 +223,6 @@ export const MCPServerForm: FC<Props> = ({ visible, initialData, onSave, onCance
183223
}
184224
}, [formData, argsText, envText]);
185225

186-
const validateForm = useCallback(
187-
(formData: MCPServerFormData) => {
188-
if (formData.name.trim() === '') {
189-
messageService.error(localize('ai.native.mcp.name.isRequired'));
190-
return false;
191-
}
192-
if (existingServers.some((server) => server.name.toLocaleLowerCase() === formData.name.toLocaleLowerCase())) {
193-
messageService.error(formatLocalize('ai.native.mcp.serverNameExists', formData.name));
194-
return false;
195-
}
196-
if (formData.type === MCP_SERVER_TYPE.SSE) {
197-
const isServerHostValid = formData.serverHost?.trim() !== '';
198-
if (!isServerHostValid) {
199-
messageService.error(localize('ai.native.mcp.serverHost.isRequired'));
200-
}
201-
return isServerHostValid;
202-
}
203-
const isCommandValid = formData.command?.trim() !== '';
204-
if (!isCommandValid) {
205-
messageService.error(localize('ai.native.mcp.command.isRequired'));
206-
}
207-
return isCommandValid;
208-
},
209-
[existingServers],
210-
);
211-
212226
return (
213227
<Modal
214228
title={initialData ? localize('ai.native.mcp.editMCPServer.title') : localize('ai.native.mcp.addMCPServer.title')}

packages/i18n/src/common/en-US.lang.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,6 +1620,9 @@ export const localizationBundle = {
16201620
'ai.native.mcp.name.isRequired': 'Server name is required',
16211621
'ai.native.mcp.command.isRequired': 'Command is required',
16221622
'ai.native.mcp.serverHost.isRequired': 'SSE URL is required',
1623+
'ai.native.mcp.manage.connections': 'Manage your MCP server connections',
1624+
'ai.native.mcp.running': 'Running',
1625+
'ai.native.mcp.stopped': 'Stopped',
16231626

16241627
// MCP View
16251628
'ai.native.mcp.tool.arguments': 'Arguments',

packages/i18n/src/common/zh-CN.lang.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,6 +1383,9 @@ export const localizationBundle = {
13831383
'ai.native.mcp.name.isRequired': '服务名称不能为空',
13841384
'ai.native.mcp.command.isRequired': '命令不能为空',
13851385
'ai.native.mcp.serverHost.isRequired': 'SSE URL 不能为空',
1386+
'ai.native.mcp.manage.connections': '管理你的 MCP 服务器连接',
1387+
'ai.native.mcp.running': '运行中',
1388+
'ai.native.mcp.stopped': '已停止',
13861389

13871390
// MCP View
13881391
'ai.native.mcp.tool.arguments': '参数',

0 commit comments

Comments
 (0)