feat: add mcp servers

This commit is contained in:
wangcham
2025-10-24 17:48:44 +00:00
parent 72ca62eae4
commit e3821b3f09
8 changed files with 396 additions and 87 deletions

View File

@@ -29,6 +29,13 @@ class MCPRouterGroup(group.RouterGroup):
servers = [self.ap.persistence_mgr.serialize_model(MCPServer, row) for row in raw_results]
servers_with_status = []
# 获取MCP工具加载器
mcp_loader = None
for loader in self.ap.tool_mgr.loaders:
if loader.__class__.__name__ == 'MCPLoader':
mcp_loader = loader
break
for server in servers:
# 设置状态
if server['enable']:
@@ -54,12 +61,18 @@ class MCPRouterGroup(group.RouterGroup):
config['args'] = extra_args.get('args', [])
config['env'] = extra_args.get('env', {})
# 从运行中的会话获取工具数量
tools_count = 0
if mcp_loader and hasattr(mcp_loader, 'sessions') and server['name'] in mcp_loader.sessions:
session = mcp_loader.sessions[server['name']]
tools_count = len(session.functions)
server_info = {
'name': server['name'],
'mode': server['mode'],
'enable': server['enable'],
'status': status,
'tools': [], # 暂时返回空数组需要连接到MCP服务器才能获取工具列表
'tools': tools_count, # 从运行中的会话获取工具数量
'config': config,
}
servers_with_status.append(server_info)
@@ -87,6 +100,7 @@ class MCPRouterGroup(group.RouterGroup):
'url':data.get('url',''),
'headers':data.get('headers',{}),
'timeout':data.get('timeout',60),
'ssereadtimeout':data.get('ssereadtimeout',300),
},
}
@@ -96,7 +110,7 @@ class MCPRouterGroup(group.RouterGroup):
return self.success()
except Exception as e:
except Exception:
print(traceback.format_exc())
@self.route('/servers/<server_name>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN)
@@ -125,6 +139,7 @@ class MCPRouterGroup(group.RouterGroup):
'url': data.get('url', extra_args.get('url','')),
'headers': data.get('headers', extra_args.get('headers',{})),
'timeout': data.get('timeout', extra_args.get('timeout',60)),
'ssereadtimeout': data.get('ssereadtimeout', extra_args.get('ssereadtimeout',300)),
})
update_data['extra_args'] = extra_args
@@ -167,26 +182,33 @@ class MCPRouterGroup(group.RouterGroup):
from .....provider.tools.loaders.mcp import RuntimeMCPSession
ctx.current_action = f'Testing connection to {server.name}'
print(server)
# 创建临时会话进行测试
session = RuntimeMCPSession(server.name, {
'name': server.name,
'mode': server.mode,
'enable': server.enable,
'extra_args': server.extra_args or {},
}, self.ap)
await session.initialize()
'name': server.name,
'mode': server.mode,
'enable': server.enable,
'url': server.extra_args.get('url',''),
'headers': server.extra_args.get('headers',{}),
'timeout': server.extra_args.get('timeout',60),
},enable=True, ap=self.ap)
await session.start()
# 获取工具列表作为测试
tools_count = len(session.functions)
tool_name_list = []
for function in session.functions:
tool_name_list.append(function.name)
ctx.current_action = f'Successfully connected. Found {tools_count} tools.'
# 关闭测试会话
await session.shutdown()
return {'status': 'success', 'tools_count': tools_count}
return {'status': 'success', 'tools_count': tools_count,'tools_names_lists':tool_name_list}
except Exception as e:
print(traceback.format_exc())
ctx.current_action = f'Connection test failed: {str(e)}'
raise e

View File

@@ -156,7 +156,7 @@ class TaskWrapper:
'state': self.task._state,
'exception': self.assume_exception().__str__() if self.assume_exception() is not None else None,
'exception_traceback': exception_traceback,
'result': self.assume_result().__str__() if self.assume_result() is not None else None,
'result': self.assume_result() if self.assume_result() is not None else None,
},
}

View File

@@ -29,9 +29,11 @@ import { httpClient } from '@/app/infra/http/HttpClient';
export default function MCPMarketComponent({
onEditServer,
toolsCountCache = {},
}: {
askInstallServer?: (githubURL: string) => void;
onEditServer?: (serverName: string) => void;
toolsCountCache?: Record<string, number>;
}) {
const { t } = useTranslation();
// const [marketServerList, setMarketServerList] = useState<MCPMarketCardVO[]>(
@@ -52,6 +54,12 @@ export default function MCPMarketComponent({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 当工具数量缓存变化时,重新获取服务器列表
useEffect(() => {
fetchInstalledServers();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [toolsCountCache]);
function initData() {
fetchInstalledServers();
// getServerList(); // GitHub 市场功能暂时注释
@@ -62,7 +70,14 @@ export default function MCPMarketComponent({
httpClient
.getMCPServers()
.then((resp) => {
const servers = resp.servers.map((server) => new MCPCardVO(server));
const servers = resp.servers.map((server) => {
const vo = new MCPCardVO(server);
// 如果缓存中有工具数量,使用缓存值覆盖
if (toolsCountCache[server.name] !== undefined) {
vo.tools = toolsCountCache[server.name];
}
return vo;
});
setInstalledServers(servers);
setLoading(false);
})
@@ -147,7 +162,7 @@ export default function MCPMarketComponent({
{/* 已安装的服务器列表 */}
<div className="mb-6">
<h2 className="text-xl font-semibold mb-4 pl-[0.8rem] pt-4">
{t('mcp.installedServers')}
{t('mcp.title')}
</h2>
<div className={`${styles.pluginListContainer}`}>
{loading ? (

View File

@@ -14,7 +14,8 @@ export class MCPCardVO {
this.mode = data.mode;
this.enable = data.enable;
this.status = data.status;
this.tools = data.tools.length;
// tools可能是数组或数字
this.tools = Array.isArray(data.tools) ? data.tools.length : (data.tools || 0);
this.error = data.error;
this.config = data.config;
}

View File

@@ -20,30 +20,17 @@ export default function MCPCardComponent({
const [enabled, setEnabled] = useState(cardVO.enable);
const [switchEnable, setSwitchEnable] = useState(true);
const [testing, setTesting] = useState(false);
const [toolsCount, setToolsCount] = useState(cardVO.tools);
function handleEnable(e: React.MouseEvent) {
e.stopPropagation(); // 阻止事件冒泡
function handleEnable(checked: boolean) {
setSwitchEnable(false);
httpClient
.toggleMCPServer(cardVO.name, !enabled)
.then((resp) => {
const taskId = resp.task_id;
// 监控任务状态
const interval = setInterval(() => {
httpClient.getAsyncTask(taskId).then((taskResp) => {
if (taskResp.runtime.done) {
clearInterval(interval);
if (taskResp.runtime.exception) {
toast.error(t('mcp.modifyFailed') + taskResp.runtime.exception);
} else {
setEnabled(!enabled);
toast.success(t('mcp.saveSuccess'));
onRefresh();
}
setSwitchEnable(true);
}
});
}, 1000);
.toggleMCPServer(cardVO.name, checked)
.then(() => {
setEnabled(checked);
toast.success(t('mcp.saveSuccess'));
onRefresh();
setSwitchEnable(true);
})
.catch((err) => {
toast.error(t('mcp.modifyFailed') + err.message);
@@ -66,7 +53,32 @@ export default function MCPCardComponent({
if (taskResp.runtime.exception) {
toast.error(t('mcp.testFailed') + taskResp.runtime.exception);
} else {
toast.success(t('mcp.testSuccess'));
// 解析测试结果获取工具数量
try {
let result: {
status?: string;
tools_count?: number;
tools_names_lists?: string[];
error?: string;
};
const rawResult: any = taskResp.runtime.result;
if (typeof rawResult === 'string') {
result = JSON.parse(rawResult.replace(/'/g, '"'));
} else {
result = rawResult as typeof result;
}
if (result.tools_count !== undefined) {
setToolsCount(result.tools_count);
toast.success(t('mcp.testSuccess') + ` - ${result.tools_count} ${t('mcp.toolsFound')}`);
} else {
toast.success(t('mcp.testSuccess'));
}
} catch (parseError) {
console.error('Failed to parse test result:', parseError);
toast.success(t('mcp.testSuccess'));
}
onRefresh();
}
setTesting(false);
@@ -147,18 +159,21 @@ export default function MCPCardComponent({
<path d="M5.32943 3.27158C6.56252 2.8332 7.9923 3.10749 8.97927 4.09446C10.1002 5.21537 10.3019 6.90741 9.5843 8.23385L20.293 18.9437L18.8788 20.3579L8.16982 9.64875C6.84325 10.3669 5.15069 10.1654 4.02952 9.04421C3.04227 8.05696 2.7681 6.62665 3.20701 5.39332L5.44373 7.63C6.02952 8.21578 6.97927 8.21578 7.56505 7.63C8.15084 7.04421 8.15084 6.09446 7.56505 5.50868L5.32943 3.27158ZM15.6968 5.15512L18.8788 3.38736L20.293 4.80157L18.5252 7.98355L16.7574 8.3371L14.6361 10.4584L13.2219 9.04421L15.3432 6.92289L15.6968 5.15512ZM8.97927 13.2868L10.3935 14.7011L5.09018 20.0044C4.69966 20.3949 4.06649 20.3949 3.67597 20.0044C3.31334 19.6417 3.28744 19.0699 3.59826 18.6774L3.67597 18.5902L8.97927 13.2868Z" />
</svg>
<div className="text-base text-black font-medium">
{t('mcp.toolCount', { count: cardVO.tools })}
{t('mcp.toolCount', { count: toolsCount })}
</div>
</div>
</div>
</div>
<div className="flex flex-col items-center justify-between h-full">
<div className="flex items-center justify-center">
<div
className="flex items-center justify-center"
onClick={(e) => e.stopPropagation()}
>
<Switch
className="cursor-pointer"
checked={enabled}
onClick={(e) => handleEnable(e)}
onCheckedChange={handleEnable}
disabled={!switchEnable}
/>
</div>

View File

@@ -42,11 +42,12 @@ import { systemInfo } from '@/app/infra/http/HttpClient';
import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@radix-ui/react-select';
SelectContent,
SelectItem,
} from "@/components/ui/select"
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
@@ -173,7 +174,7 @@ export default function PluginConfigPage() {
fetchPluginSystemStatus();
}, [t]);
//这个是旧版本的测试github url下面重写了一个新版本的watchTask函数用来检测Mcp
function watchTask(taskId: number) {
let alreadySuccess = false;
console.log('taskId:', taskId);
@@ -200,7 +201,53 @@ export default function PluginConfigPage() {
}
});
}, 1000);
}
function watchTestMCPTask(taskId: number) {
let alreadyHandled = false;
console.log('Watching MCP test task:', taskId);
const interval = setInterval(() => {
httpClient.getAsyncTask(taskId).then((resp) => {
console.log('task status:', resp);
// 若任务已完成
if (resp.runtime && resp.runtime.done) {
clearInterval(interval);
if (resp.runtime.exception) {
// 任务失败
toast.error(`测试失败: ${resp.runtime.exception}`);
} else if (resp.runtime.result) {
// 任务成功
const result = resp.runtime.result as {
status?: string;
tools_count?: number;
tools_names_lists?: string[];
error?: string;
};
const names = result.tools_names_lists || [];
if (!alreadyHandled) {
alreadyHandled = true;
const names = result.tools_names_lists || [];
toast.success(`连接成功,找到 ${names.length} 个工具`);
console.log('工具列表:', names);
}
} else {
// 没结果但标记为完成
toast.error('测试任务完成但未返回结果');
}
}
}).catch((err) => {
console.error('任务状态获取失败:', err);
toast.error('获取任务状态失败');
clearInterval(interval);
});
}, 1000);
}
const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null);
const mcpComponentRef = useRef<MCPComponentRef>(null);
const [mcpTesting, setMcpTesting] = useState(false);
@@ -210,6 +257,14 @@ export default function PluginConfigPage() {
const [isEditMode, setIsEditMode] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
// MCP测试结果状态
const [mcpTestStatus, setMcpTestStatus] = useState<'idle' | 'testing' | 'success' | 'failed'>('idle');
const [mcpToolNames, setMcpToolNames] = useState<string[]>([]);
const [mcpTestError, setMcpTestError] = useState<string>('');
// 缓存每个服务器测试后的工具数量
const [serverToolsCache, setServerToolsCache] = useState<Record<string, number>>({});
// 强制清理 body 样式以修复 Dialog 关闭后点击失效的问题
useEffect(() => {
console.log('[Dialog Debug] States:', {
@@ -397,43 +452,140 @@ export default function PluginConfigPage() {
}
async function loadServerForEdit(serverName: string) {
try {
const resp = await httpClient.getMCPServer(serverName);
const server = resp.server ?? resp; // 有的接口包了一层,有的直接返回对象
try {
const resp = await httpClient.getMCPServer(serverName);
const server = resp.server ?? resp; // 有的接口包了一层,有的直接返回对象
console.log('Loaded server for edit:', server);
console.log('Loaded server for edit:', server);
// 填充表单数据
form.setValue('name', server.name);
form.setValue('url', server.extra_args?.url || '');
form.setValue('timeout', server.extra_args?.timeout || 30);
form.setValue('ssereadtimeout', server.extra_args?.ssereadtimeout || 300);
// 填充表单数据
form.setValue('name', server.name);
form.setValue('url', server.extra_args?.url || '');
form.setValue('timeout', server.extra_args?.timeout || 30);
form.setValue('ssereadtimeout', 300);
// 填充 headers
if (server.extra_args?.headers) {
const headers = Object.entries(server.extra_args.headers).map(
([key, value]) => ({
key,
type: 'string' as const,
value: String(value),
}),
);
setExtraArgs(headers);
form.setValue('extra_args', headers);
}
// 填充 headers
if (server.extra_args?.headers) {
const headers = Object.entries(server.extra_args.headers).map(
([key, value]) => ({
key,
type: 'string' as const,
value: String(value),
}),
);
setExtraArgs(headers);
form.setValue('extra_args', headers);
// 重置测试状态
setMcpTestStatus('testing');
setMcpToolNames([]);
setMcpTestError('');
// 打开对话框
setEditingServerName(serverName);
setIsEditMode(true);
setMcpSSEModalOpen(true);
// 在这里测试工具连接状态
try {
const res = await httpClient.testMCPServer(server.name);
if (res.task_id) {
const taskId = res.task_id;
// 监听任务完成
const interval = setInterval(() => {
httpClient.getAsyncTask(taskId).then((taskResp) => {
console.log('Task response:', taskResp);
if (taskResp.runtime && taskResp.runtime.done) {
clearInterval(interval);
console.log('Task completed. Runtime:', taskResp.runtime);
console.log('Result:', taskResp.runtime.result);
console.log('Exception:', taskResp.runtime.exception);
if (taskResp.runtime.exception) {
// 测试失败
console.log('Test failed with exception');
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError(taskResp.runtime.exception || '未知错误');
} else if (taskResp.runtime.result) {
// 测试成功 - 后端可能返回字符串或对象
try {
let result: {
status?: string;
tools_count?: number;
tools_names_lists?: string[];
error?: string;
};
// 如果result是字符串需要先解析
const rawResult: any = taskResp.runtime.result;
if (typeof rawResult === 'string') {
console.log('Result is string, parsing...');
result = JSON.parse(rawResult.replace(/'/g, '"'));
} else {
result = rawResult as typeof result;
}
console.log('Parsed result:', result);
console.log('tools_names_lists:', result.tools_names_lists);
console.log('tools_names_lists length:', result.tools_names_lists?.length);
if (result.tools_names_lists && result.tools_names_lists.length > 0) {
console.log('Test success with', result.tools_names_lists.length, 'tools');
setMcpTestStatus('success');
setMcpToolNames(result.tools_names_lists);
// 保存工具数量到缓存
setServerToolsCache(prev => ({
...prev,
[server.name]: result.tools_names_lists!.length
}));
} else {
console.log('Test failed: no tools found');
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError('未找到任何工具');
}
} catch (parseError) {
console.error('Failed to parse result:', parseError);
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError('解析测试结果失败');
}
} else {
// 没结果
console.log('Test failed: no result');
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError('测试未返回结果');
}
}
}).catch((err) => {
console.error('获取任务状态失败:', err);
clearInterval(interval);
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError(err.message || '获取任务状态失败');
});
}, 1000);
} else {
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError('未获取到任务ID');
}
} catch (error) {
console.error('Failed to test server:', error);
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError((error as Error).message || '测试连接时发生错误');
}
} catch (error) {
console.error('Failed to load server:', error);
toast.error(t('mcp.loadFailed'));
}
//在这里返回mcp里的tools
const tools = await httpClient.getMCPTools(server.name);
setEditingServerName(serverName);
setIsEditMode(true);
setMcpSSEModalOpen(true);
} catch (error) {
console.error('Failed to load server:', error);
toast.error(t('mcp.loadFailed'));
}
}
async function handleFormSubmit(value: z.infer<typeof formSchema>) {
const extraArgsObj: Record<string, string | number | boolean> = {};
@@ -458,6 +610,7 @@ export default function PluginConfigPage() {
url: value.url,
headers: extraArgsObj as Record<string, string>,
timeout: value.timeout,
ssereadtimeout: value.ssereadtimeout,
};
if (isEditMode && editingServerName) {
@@ -782,6 +935,7 @@ export default function PluginConfigPage() {
onEditServer={(serverName) => {
loadServerForEdit(serverName);
}}
toolsCountCache={serverToolsCache}
/>
</TabsContent>
</Tabs>
@@ -868,10 +1022,10 @@ export default function PluginConfigPage() {
>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('plugins.confirmDeleteTitle')}</DialogTitle>
<DialogTitle>{t('mcp.confirmDeleteTitle')}</DialogTitle>
</DialogHeader>
<DialogDescription>
{t('plugins.deleteConfirmation')}
{t('mcp.confirmDeleteServer')}
</DialogDescription>
<DialogFooter>
<Button
@@ -907,6 +1061,99 @@ export default function PluginConfigPage() {
{isEditMode ? t('mcp.editServer') : t('mcp.createServer')}
</DialogTitle>
</DialogHeader>
{/* 测试结果显示区域 - 仅在编辑模式显示 */}
{isEditMode && (
<div className="mb-4 p-3 rounded-lg border">
{mcpTestStatus === 'testing' && (
<div className="flex items-center gap-2 text-blue-600">
<svg
className="w-5 h-5 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span className="font-medium">{t('mcp.testing')}</span>
</div>
)}
{mcpTestStatus === 'success' && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-green-600">
<svg
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span className="font-medium">
{t('mcp.connectionSuccess')} - {mcpToolNames.length} {t('mcp.toolsFound')}
</span>
</div>
<div className="flex flex-wrap gap-1 mt-2">
{mcpToolNames.map((toolName, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 text-xs rounded-md"
>
{toolName}
</span>
))}
</div>
</div>
)}
{mcpTestStatus === 'failed' && (
<div className="space-y-1">
<div className="flex items-center gap-2 text-red-600">
<svg
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span className="font-medium">{t('mcp.connectionFailed')}</span>
</div>
{mcpTestError && (
<div className="text-sm text-red-500 pl-7">
{mcpTestError}
</div>
)}
</div>
)}
</div>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleFormSubmit)}
@@ -958,11 +1205,16 @@ export default function PluginConfigPage() {
<FormField
control={form.control}
name="ssereadtimeout"
render={(field) => (
render={({ field }) => (
<FormItem>
<FormLabel>{t('mcp.ssereadtimeout')}</FormLabel>
<FormLabel>{t('mcp.sseTimeout')}</FormLabel>
<FormControl>
<Input placeholder={t('mcp.sseTimeout')} {...field} />
<Input
type="number"
placeholder={t('mcp.sseTimeoutDescription')}
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -990,7 +1242,7 @@ export default function PluginConfigPage() {
<SelectTrigger className="w-[120px] bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('models.type')} />
</SelectTrigger>
<SelectContent>
<SelectContent className="bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectItem value="string">
{t('models.string')}
</SelectItem>
@@ -1034,7 +1286,7 @@ export default function PluginConfigPage() {
</Button>
</div>
<FormDescription>
{t('llm.extraParametersDescription')}
{t('mcp.extraParametersDescription')}
</FormDescription>
<FormMessage />
</FormItem>

View File

@@ -521,8 +521,8 @@ export class BackendClient extends BaseHttpClient {
serverName: string,
target_enabled: boolean,
): Promise<AsyncTaskCreatedResp> {
return this.put(`/api/v1/mcp/servers/${serverName}/toggle`, {
target_enabled,
return this.put(`/api/v1/mcp/servers/${serverName}`, {
enable: target_enabled,
});
}

View File

@@ -276,6 +276,8 @@ const zhHans = {
createServer: '创建MCP服务器',
editServer: '编辑MCP服务器',
deleteServer: '删除MCP服务器',
confirmDeleteServer: '你确定要删除此MCP服务器吗',
confirmDeleteTitle: '删除MCP服务器',
getServerListError: '获取MCP服务器列表失败',
serverName: '服务器名称',
serverMode: '连接模式',
@@ -304,7 +306,9 @@ const zhHans = {
testing: '测试中...',
testSuccess: '连接测试成功',
testFailed: '连接测试失败:',
confirmDeleteServer: '你确定要删除MCP服务器{{name}})吗?',
connectionSuccess: '连接成功',
connectionFailed: '连接失败',
toolsFound: '个工具',
deleteSuccess: '删除成功',
deleteError: '删除失败:',
saveSuccess: '保存成功',
@@ -336,8 +340,6 @@ const zhHans = {
add: '添加',
name: '名称',
nameExplained: '用于区分不同的MCP服务器实例',
mcpDescription: '描述',
descriptionExplained: '简要描述这个MCP服务器的功能或用途',
sseURL: 'SSE URL',
sseHeaders: 'SSE Headers',
nameRequired: '名称不能为空',
@@ -348,6 +350,8 @@ const zhHans = {
enterTimeout: '输入超时时间,单位为毫秒',
installFromSSE: '从SSE安装',
sseTimeout: 'SSE超时时间',
sseTimeoutDescription: '用于建立SSE连接的超时时间',
extraParametersDescription: '额外参数用于配置MCP服务器的特定行为',
},
pipelines: {
title: '流水线',