refactor: mcp server datastructure

This commit is contained in:
Junyan Qin
2025-11-04 16:13:03 +08:00
parent bc1fbfa190
commit c1c03f11b4
13 changed files with 155 additions and 208 deletions

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import quart
import traceback
from ... import group
@@ -13,36 +14,18 @@ class MCPRouterGroup(group.RouterGroup):
async def _() -> str:
"""获取MCP服务器列表"""
if quart.request.method == 'GET':
servers = await self.ap.mcp_service.get_mcp_servers()
servers = await self.ap.mcp_service.get_mcp_servers(contain_runtime_info=True)
servers_with_status = []
# 获取MCP工具加载器
mcp_loader = self.ap.tool_mgr.mcp_tool_loader
for server in servers:
# 从运行中的会话获取工具数量
tools_count = 0
if mcp_loader:
session = mcp_loader.sessions.get(server['name'])
if session:
tools_count = len(session.functions)
server_info = {
**server,
'tools': tools_count,
}
servers_with_status.append(server_info)
return self.success(data={'servers': servers_with_status})
return self.success(data={'servers': servers})
elif quart.request.method == 'POST':
data = await quart.request.json
data = data['source']
try:
uuid = await self.ap.mcp_service.create_mcp_server(data)
return self.success(data={'uuid': uuid})
except Exception as e:
traceback.print_exc()
return self.http_status(500, -1, f'Failed to create MCP server: {str(e)}')
@self.route('/servers/<server_name>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN)

View File

@@ -40,7 +40,6 @@ class RuntimeMCPServer:
self.session = RuntimeMCPSession(
self.mcp_server_entity.name, mixed_config, self.mcp_server_entity.enable, self.ap
)
await self.session.initialize()
await self.session.start()
async def _test_mcp_server_task(self, task_context: taskmgr.TaskContext):
@@ -102,14 +101,29 @@ class MCPService:
def __init__(self, ap: app.Application) -> None:
self.ap = ap
async def get_mcp_servers(self) -> list[dict]:
async def get_mcp_servers(self, contain_runtime_info: bool = False) -> list[dict]:
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_mcp.MCPServer))
servers = result.all()
return [self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, server) for server in servers]
serialized_servers = [
self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, server) for server in servers
]
if contain_runtime_info:
for server in serialized_servers:
session = self.ap.tool_mgr.mcp_tool_loader.get_session(server['name'])
runtime_info = None
if session:
runtime_info = session.get_runtime_info_dict()
server['runtime_info'] = runtime_info if runtime_info else None
return serialized_servers
async def create_mcp_server(self, server_data: dict) -> str:
server_data['uuid'] = str(uuid.uuid4())
print('server_data:', server_data)
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_mcp.MCPServer).values(server_data))
result = await self.ap.persistence_mgr.execute_async(

View File

@@ -58,7 +58,7 @@ class PluginRuntimeConnector:
async def heartbeat_loop(self):
while True:
await asyncio.sleep(10)
await asyncio.sleep(20)
try:
await self.ping_plugin_runtime()
self.ap.logger.debug('Heartbeat to plugin runtime success.')

View File

@@ -33,6 +33,10 @@ class RuntimeMCPSession:
enable: bool
connected: bool
last_test_error_message: str
def __init__(self, server_name: str, server_config: dict, enable: bool, ap: app.Application):
self.server_name = server_name
self.server_config = server_config
@@ -43,6 +47,9 @@ class RuntimeMCPSession:
self.exit_stack = AsyncExitStack()
self.functions = []
self.connected = False
self.last_test_error_message = ''
async def _init_stdio_python_server(self):
server_params = StdioServerParameters(
command=self.server_config['command'],
@@ -64,6 +71,7 @@ class RuntimeMCPSession:
self.server_config['url'],
headers=self.server_config.get('headers', {}),
timeout=self.server_config.get('timeout', 10),
sse_read_timeout=self.server_config.get('ssereadtimeout', 30),
)
)
@@ -73,47 +81,66 @@ class RuntimeMCPSession:
await self.session.initialize()
async def initialize(self):
pass
async def start(self):
if not self.enable:
return
if self.server_config['mode'] == 'stdio':
await self._init_stdio_python_server()
elif self.server_config['mode'] == 'sse':
await self._init_sse_server()
else:
raise ValueError(f'无法识别 MCP 服务器类型: {self.server_name}: {self.server_config}')
try:
if self.server_config['mode'] == 'stdio':
await self._init_stdio_python_server()
elif self.server_config['mode'] == 'sse':
await self._init_sse_server()
else:
raise ValueError(f'无法识别 MCP 服务器类型: {self.server_name}: {self.server_config}')
tools = await self.session.list_tools()
tools = await self.session.list_tools()
self.ap.logger.debug(f'获取 MCP 工具: {tools}')
self.ap.logger.debug(f'获取 MCP 工具: {tools}')
for tool in tools.tools:
for tool in tools.tools:
async def func(*, _tool=tool, **kwargs):
result = await self.session.call_tool(_tool.name, kwargs)
if result.isError:
raise Exception(result.content[0].text)
return result.content[0].text
async def func(*, _tool=tool, **kwargs):
result = await self.session.call_tool(_tool.name, kwargs)
if result.isError:
raise Exception(result.content[0].text)
return result.content[0].text
func.__name__ = tool.name
func.__name__ = tool.name
self.functions.append(
resource_tool.LLMTool(
name=tool.name,
human_desc=tool.description,
description=tool.description,
parameters=tool.inputSchema,
func=func,
self.functions.append(
resource_tool.LLMTool(
name=tool.name,
human_desc=tool.description,
description=tool.description,
parameters=tool.inputSchema,
func=func,
)
)
)
self.connected = True
self.last_test_error_message = ''
except Exception as e:
self.connected = False
self.last_test_error_message = str(e)
raise e
def get_tools(self) -> list[resource_tool.LLMTool]:
return self.functions
def get_runtime_info_dict(self) -> dict:
return {
'connected': self.connected,
'error_message': self.last_test_error_message,
'tool_count': len(self.get_tools()),
'tools': [
{
'name': tool.name,
'description': tool.description,
}
for tool in self.get_tools()
],
}
async def shutdown(self):
"""关闭会话并清理资源"""
try:
@@ -156,9 +183,9 @@ class MCPLoader(loader.ToolLoader):
servers = result.all()
for server in servers:
server_config = self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, server)
config = self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, server)
async def load_mcp_server_task():
async def load_mcp_server_task(server_config: dict):
self.ap.logger.debug(f'Loading MCP server {server_config}')
try:
session = await self.load_mcp_server(server_config)
@@ -180,7 +207,7 @@ class MCPLoader(loader.ToolLoader):
self.ap.logger.debug(f'Started MCP server {server_config["name"]}({server_config["uuid"]})')
task = asyncio.create_task(load_mcp_server_task())
task = asyncio.create_task(load_mcp_server_task(config))
self._startup_load_tasks.append(task)
async def load_mcp_server(self, server_config: dict) -> RuntimeMCPSession:
@@ -207,7 +234,6 @@ class MCPLoader(loader.ToolLoader):
}
session = RuntimeMCPSession(name, mixed_config, enable, self.ap)
await session.initialize()
return session

View File

@@ -1,7 +1,6 @@
'use client';
import { useEffect, useState } from 'react';
import styles from '@/app/home/plugins/plugins.module.css';
import MCPCardComponent from '@/app/home/plugins/mcp-server/mcp-card/MCPCardComponent';
import { MCPCardVO } from '@/app/home/plugins/mcp-server/MCPCardVO';
import { useTranslation } from 'react-i18next';
@@ -55,16 +54,14 @@ export default function MCPComponent({
}
return (
<div className={`${styles.marketComponentBody}`}>
<div className="w-full h-full">
{/* 已安装的服务器列表 */}
<div className="mb-6">
<div className={`${styles.pluginListContainer}`}>
<div className="mb-[2rem]">
<div className="w-full px-[0.8rem] pt-[2rem] grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{loading ? (
<div style={{ textAlign: 'center', padding: '20px' }}>
{t('mcp.loading')}
</div>
<div className="text-center p-[2rem]">{t('mcp.loading')}</div>
) : installedServers.length === 0 ? (
<div style={{ textAlign: 'center', padding: '20px' }}>
<div className="text-center p-[2rem]">
{t('mcp.noServerInstalled')}
</div>
) : (

View File

@@ -6,6 +6,7 @@ import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { RefreshCcw, Wrench } from 'lucide-react';
export default function MCPCardComponent({
cardVO,
@@ -36,36 +37,6 @@ export default function MCPCardComponent({
setEnabled(cardVO.enable);
}, [cardVO.name, cardVO.status, cardVO.error, cardVO.tools, cardVO.enable]);
function getStatusColor(): string {
switch (status) {
case 'connected':
return 'text-green-600';
case 'disconnected':
return 'text-gray-500';
case 'error':
return 'text-red-600';
case 'disabled':
return 'text-gray-400';
default:
return 'text-gray-500';
}
}
function getStatusIcon(): string {
switch (status) {
case 'connected':
return 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z';
case 'disconnected':
return 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z';
case 'error':
return 'M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z';
case 'disabled':
return 'M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636';
default:
return 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z';
}
}
function handleEnable(checked: boolean) {
setSwitchEnable(false);
httpClient
@@ -154,55 +125,32 @@ export default function MCPCardComponent({
return (
<div
className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] p-[1.2rem] cursor-pointer"
className="w-[100%] h-[10rem] bg-white dark:bg-[#1f1f22] rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] dark:shadow-none p-[1.2rem] cursor-pointer transition-all duration-200 hover:shadow-[0px_2px_8px_0_rgba(0,0,0,0.1)] dark:hover:shadow-none"
onClick={onCardClick}
>
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
<svg
className="w-16 h-16 text-[#2288ee]"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
width="64"
height="64"
fill="rgba(70,146,221,1)"
>
<path d="M13.5 2C13.5 2.82843 14.1716 3.5 15 3.5C15.8284 3.5 16.5 2.82843 16.5 2C16.5 1.17157 15.8284 0.5 15 0.5C14.1716 0.5 13.5 1.17157 13.5 2ZM8.5 8C8.5 8.82843 9.17157 9.5 10 9.5C10.8284 9.5 11.5 8.82843 11.5 8C11.5 7.17157 10.8284 6.5 10 6.5C9.17157 6.5 8.5 7.17157 8.5 8ZM1.5 14C1.5 14.8284 2.17157 15.5 3 15.5C3.82843 15.5 4.5 14.8284 4.5 14C4.5 13.1716 3.82843 12.5 3 12.5C2.17157 12.5 1.5 13.1716 1.5 14ZM19.5 14C19.5 14.8284 20.1716 15.5 21 15.5C21.8284 15.5 22.5 14.8284 22.5 14C22.5 13.1716 21.8284 12.5 21 12.5C20.1716 12.5 19.5 13.1716 19.5 14ZM8.5 20C8.5 20.8284 9.17157 21.5 10 21.5C10.8284 21.5 11.5 20.8284 11.5 20C11.5 19.1716 10.8284 19 10 19C9.17157 19 8.5 19.1716 8.5 20ZM2.5 8L6.5 8L6.5 10L2.5 10L2.5 8ZM13.5 8L17.5 8L17.5 10L13.5 10L13.5 8ZM8.5 2L8.5 6L10.5 6L10.5 2L8.5 2ZM8.5 14L8.5 18L10.5 18L10.5 14L8.5 14ZM2.5 14L6.5 14L6.5 16L2.5 16L2.5 14ZM13.5 14L17.5 14L17.5 16L13.5 16L13.5 14Z"></path>
<path d="M17.6567 14.8284L16.2425 13.4142L17.6567 12C19.2188 10.4379 19.2188 7.90524 17.6567 6.34314C16.0946 4.78105 13.5619 4.78105 11.9998 6.34314L10.5856 7.75736L9.17139 6.34314L10.5856 4.92893C12.9287 2.58578 16.7277 2.58578 19.0709 4.92893C21.414 7.27208 21.414 11.0711 19.0709 13.4142L17.6567 14.8284ZM14.8282 17.6569L13.414 19.0711C11.0709 21.4142 7.27189 21.4142 4.92875 19.0711C2.5856 16.7279 2.5856 12.9289 4.92875 10.5858L6.34296 9.17157L7.75717 10.5858L6.34296 12C4.78086 13.5621 4.78086 16.0948 6.34296 17.6569C7.90506 19.2189 10.4377 19.2189 11.9998 17.6569L13.414 16.2426L14.8282 17.6569ZM14.8282 7.75736L16.2425 9.17157L9.17139 16.2426L7.75717 14.8284L14.8282 7.75736Z"></path>
</svg>
<div className="w-full h-full flex flex-col items-start justify-between gap-[0.6rem]">
<div className="flex flex-col items-start justify-start">
<div className="flex flex-col items-start justify-start">
<div className="flex flex-row items-center justify-start gap-[0.4rem]">
<div className="text-[1.2rem] text-black">{cardVO.name}</div>
<Badge variant="outline" className="text-[0.7rem]">
{cardVO.mode.toUpperCase()}
</Badge>
</div>
</div>
<div className="flex flex-row items-center justify-start gap-[0.4rem] mt-1">
<svg
className={`w-4 h-4 ${getStatusColor()}`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d={getStatusIcon()}
/>
</svg>
<div className={`text-[0.8rem] ${getStatusColor()}`}>
{status === 'connected' && t('mcp.statusConnected')}
{status === 'disconnected' && t('mcp.statusDisconnected')}
{status === 'error' && t('mcp.statusError')}
{status === 'disabled' && t('mcp.statusDisabled')}
<div className="text-[1.2rem] text-black dark:text-[#f0f0f0] font-medium">
{cardVO.name}
</div>
</div>
</div>
{error && (
<div className="text-[0.7rem] text-red-500 line-clamp-2 mt-1">
<div className="text-[0.7rem] text-red-500 dark:text-red-400 line-clamp-2 mt-1">
{error}
</div>
)}
@@ -210,15 +158,8 @@ export default function MCPCardComponent({
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem]">
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
<svg
className="w-[1.2rem] h-[1.2rem] text-black"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<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">
<Wrench className="w-5 h-5" />
<div className="text-base text-black dark:text-[#f0f0f0] font-medium">
{t('mcp.toolCount', { count: toolsCount })}
</div>
</div>
@@ -246,22 +187,7 @@ export default function MCPCardComponent({
onClick={(e) => handleTest(e)}
disabled={testing}
>
<svg
className={`w-4 h-4 text-gray-600 ${
testing ? 'animate-spin' : ''
}`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
<RefreshCcw className="w-4 h-4" />
</Button>
</div>
</div>

View File

@@ -396,25 +396,20 @@ export default function PluginConfigPage() {
const server = resp.server ?? resp;
console.log('Loaded server for edit:', server);
const extraArgs = server.extra_args as
| Record<string, unknown>
| undefined;
const extraArgs = server.extra_args;
form.setValue('name', server.name);
form.setValue('url', (extraArgs?.url as string) || '');
form.setValue('timeout', (extraArgs?.timeout as number) || 30);
form.setValue(
'ssereadtimeout',
(extraArgs?.ssereadtimeout as number) || 300,
);
form.setValue('url', extraArgs.url);
form.setValue('timeout', extraArgs.timeout);
form.setValue('ssereadtimeout', extraArgs.ssereadtimeout);
if (extraArgs?.headers) {
const headers = Object.entries(
extraArgs.headers as Record<string, unknown>,
).map(([key, value]) => ({
key,
type: 'string' as const,
value: String(value),
}));
if (extraArgs.headers) {
const headers = Object.entries(extraArgs.headers).map(
([key, value]) => ({
key,
type: 'string' as const,
value: String(value),
}),
);
setExtraArgs(headers);
form.setValue('extra_args', headers);
}
@@ -569,7 +564,17 @@ export default function PluginConfigPage() {
await httpClient.updateMCPServer(editingServerName, serverConfig);
toast.success(t('mcp.updateSuccess'));
} else {
await httpClient.createMCPServer(serverConfig);
await httpClient.createMCPServer({
extra_args: {
url: value.url,
headers: extraArgsObj as Record<string, string>,
timeout: value.timeout,
ssereadtimeout: value.ssereadtimeout,
},
name: value.name,
mode: 'sse' as const,
enable: true,
});
toast.success(t('mcp.createSuccess'));
}
@@ -927,9 +932,6 @@ export default function PluginConfigPage() {
}}
/>
</TabsContent>
{/* <TabsContent value="mcp">
<MCPComponent ref={mcpComponentRef} />
</TabsContent> */}
<TabsContent value="mcp-servers">
<MCPServerComponent
key={refreshKey}

View File

@@ -318,29 +318,28 @@ export interface ApiRespMCPServer {
server: MCPServer;
}
export interface MCPServer {
extra_args: Record<string, unknown>;
name: string;
mode: 'stdio' | 'sse';
enable: boolean;
config: MCPServerConfig;
status: 'connected' | 'disconnected' | 'error' | 'disabled';
tools: MCPTool[];
error?: string;
export interface MCPServerExtraArgsSSE {
url: string;
headers: Record<string, string>;
timeout: number;
ssereadtimeout: number;
}
export interface MCPServerConfig {
export interface MCPServerRuntimeInfo {
connected: boolean;
error_message: string;
tool_count: number;
}
export interface MCPServer {
uuid?: string;
name: string;
mode: 'stdio' | 'sse';
enable: boolean;
// stdio mode
command?: string;
args?: string[];
env?: Record<string, string>;
// sse mode
url?: string;
headers?: Record<string, string>;
timeout?: number;
extra_args: MCPServerExtraArgsSSE;
runtime_info?: MCPServerRuntimeInfo;
created_at?: string;
updated_at?: string;
}
export interface MCPTool {

View File

@@ -35,7 +35,7 @@ import {
ApiRespPluginSystemStatus,
ApiRespMCPServers,
ApiRespMCPServer,
MCPServerConfig,
MCPServer,
} from '@/app/infra/entities/api';
import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest';
import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
@@ -500,15 +500,13 @@ export class BackendClient extends BaseHttpClient {
return this.get(`/api/v1/mcp/servers/${serverName}`);
}
public createMCPServer(
server: MCPServerConfig,
): Promise<AsyncTaskCreatedResp> {
return this.post('/api/v1/mcp/servers', { source: server });
public createMCPServer(server: MCPServer): Promise<AsyncTaskCreatedResp> {
return this.post('/api/v1/mcp/servers', server);
}
public updateMCPServer(
serverName: string,
server: Partial<MCPServerConfig>,
server: Partial<MCPServer>,
): Promise<AsyncTaskCreatedResp> {
return this.put(`/api/v1/mcp/servers/${serverName}`, server);
}

View File

@@ -284,7 +284,7 @@ const enUS = {
},
mcp: {
title: 'MCP Management',
createServer: 'Create MCP Server',
createServer: 'Add MCP Server',
editServer: 'Edit MCP Server',
deleteServer: 'Delete MCP Server',
confirmDeleteServer: 'Are you sure you want to delete this MCP server?',
@@ -347,7 +347,8 @@ const enUS = {
nameRequired: 'Name cannot be empty',
sseTimeout: 'SSE Timeout',
sseTimeoutDescription: 'Timeout for establishing SSE connection',
extraParametersDescription: 'Additional parameters for configuring specific MCP server behavior',
extraParametersDescription:
'Additional parameters for configuring specific MCP server behavior',
timeoutMustBeNumber: 'Timeout must be a number',
timeoutNonNegative: 'Timeout cannot be negative',
sseTimeoutMustBeNumber: 'SSE timeout must be a number',

View File

@@ -286,7 +286,7 @@ const jaJP = {
},
mcp: {
title: 'MCP管理',
createServer: 'MCPサーバーを作成',
createServer: 'MCPサーバーを追加',
editServer: 'MCPサーバーを編集',
deleteServer: 'MCPサーバーを削除',
confirmDeleteServer: 'このMCPサーバーを削除してもよろしいですか',
@@ -349,7 +349,8 @@ const jaJP = {
nameRequired: '名前は必須です',
sseTimeout: 'SSEタイムアウト',
sseTimeoutDescription: 'SSE接続を確立するためのタイムアウト',
extraParametersDescription: 'MCPサーバーの特定の動作を設定するための追加パラメータ',
extraParametersDescription:
'MCPサーバーの特定の動作を設定するための追加パラメータ',
timeoutMustBeNumber: 'タイムアウトは数値である必要があります',
timeoutNonNegative: 'タイムアウトは負の数にできません',
sseTimeoutMustBeNumber: 'SSEタイムアウトは数値である必要があります',

View File

@@ -272,20 +272,20 @@ const zhHans = {
},
mcp: {
title: 'MCP管理',
createServer: '创建MCP服务器',
editServer: '编辑MCP服务器',
deleteServer: '删除MCP服务器',
confirmDeleteServer: '你确定要删除此MCP服务器吗',
confirmDeleteTitle: '删除MCP服务器',
getServerListError: '获取MCP服务器列表失败',
createServer: '添加 MCP 服务器',
editServer: '修改 MCP 服务器',
deleteServer: '删除 MCP 服务器',
confirmDeleteServer: '你确定要删除此 MCP 服务器吗?',
confirmDeleteTitle: '删除 MCP 服务器',
getServerListError: '获取 MCP 服务器列表失败:',
serverName: '服务器名称',
serverMode: '连接模式',
stdio: 'Stdio模式',
sse: 'SSE模式',
noServerInstalled: '暂未配置任何MCP服务器',
noServerInstalled: '暂未配置任何 MCP 服务器',
serverNameRequired: '服务器名称不能为空',
commandRequired: '命令不能为空',
urlRequired: 'URL不能为空',
urlRequired: 'URL 不能为空',
timeoutMustBePositive: '超时时间必须是正数',
command: '命令',
args: '参数',

View File

@@ -270,7 +270,7 @@ const zhHant = {
},
mcp: {
title: 'MCP管理',
createServer: '建立MCP伺服器',
createServer: '新增MCP伺服器',
editServer: '編輯MCP伺服器',
deleteServer: '刪除MCP伺服器',
confirmDeleteServer: '您確定要刪除此MCP伺服器嗎',