From c1c03f11b4a4cf5582ae5cfe0449f848648829e1 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 4 Nov 2025 16:13:03 +0800 Subject: [PATCH] refactor: mcp server datastructure --- .../http/controller/groups/resources/mcp.py | 25 +---- pkg/api/http/service/mcp.py | 20 +++- pkg/plugin/connector.py | 2 +- pkg/provider/tools/loaders/mcp.py | 86 +++++++++------ .../plugins/mcp-server/MCPServerComponent.tsx | 13 +-- .../mcp-server/mcp-card/MCPCardComponent.tsx | 100 +++--------------- web/src/app/home/plugins/page.tsx | 44 ++++---- web/src/app/infra/entities/api/index.ts | 35 +++--- web/src/app/infra/http/BackendClient.ts | 10 +- web/src/i18n/locales/en-US.ts | 5 +- web/src/i18n/locales/ja-JP.ts | 5 +- web/src/i18n/locales/zh-Hans.ts | 16 +-- web/src/i18n/locales/zh-Hant.ts | 2 +- 13 files changed, 155 insertions(+), 208 deletions(-) diff --git a/pkg/api/http/controller/groups/resources/mcp.py b/pkg/api/http/controller/groups/resources/mcp.py index 97788583..46559bc9 100644 --- a/pkg/api/http/controller/groups/resources/mcp.py +++ b/pkg/api/http/controller/groups/resources/mcp.py @@ -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/', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN) diff --git a/pkg/api/http/service/mcp.py b/pkg/api/http/service/mcp.py index ea120718..d6e41ab4 100644 --- a/pkg/api/http/service/mcp.py +++ b/pkg/api/http/service/mcp.py @@ -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( diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index 0100a9b5..9b362db2 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -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.') diff --git a/pkg/provider/tools/loaders/mcp.py b/pkg/provider/tools/loaders/mcp.py index fc9db294..00dece7b 100644 --- a/pkg/provider/tools/loaders/mcp.py +++ b/pkg/provider/tools/loaders/mcp.py @@ -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 diff --git a/web/src/app/home/plugins/mcp-server/MCPServerComponent.tsx b/web/src/app/home/plugins/mcp-server/MCPServerComponent.tsx index 68fad521..0a0b85a6 100644 --- a/web/src/app/home/plugins/mcp-server/MCPServerComponent.tsx +++ b/web/src/app/home/plugins/mcp-server/MCPServerComponent.tsx @@ -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 ( -
+
{/* 已安装的服务器列表 */} -
-
+
+
{loading ? ( -
- {t('mcp.loading')} -
+
{t('mcp.loading')}
) : installedServers.length === 0 ? ( -
+
{t('mcp.noServerInstalled')}
) : ( diff --git a/web/src/app/home/plugins/mcp-server/mcp-card/MCPCardComponent.tsx b/web/src/app/home/plugins/mcp-server/mcp-card/MCPCardComponent.tsx index 87832ce1..b83b83ed 100644 --- a/web/src/app/home/plugins/mcp-server/mcp-card/MCPCardComponent.tsx +++ b/web/src/app/home/plugins/mcp-server/mcp-card/MCPCardComponent.tsx @@ -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 (
- +
-
{cardVO.name}
- - {cardVO.mode.toUpperCase()} - -
-
- -
- - - -
- {status === 'connected' && t('mcp.statusConnected')} - {status === 'disconnected' && t('mcp.statusDisconnected')} - {status === 'error' && t('mcp.statusError')} - {status === 'disabled' && t('mcp.statusDisabled')} +
+ {cardVO.name} +
{error && ( -
+
{error}
)} @@ -210,15 +158,8 @@ export default function MCPCardComponent({
- - - -
+ +
{t('mcp.toolCount', { count: toolsCount })}
@@ -246,22 +187,7 @@ export default function MCPCardComponent({ onClick={(e) => handleTest(e)} disabled={testing} > - - - +
diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index bca70c3c..d29c1a86 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -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 - | 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, - ).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, + 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() { }} /> - {/* - - */} ; - 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; + 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; - // sse mode - url?: string; - headers?: Record; - timeout?: number; + extra_args: MCPServerExtraArgsSSE; + runtime_info?: MCPServerRuntimeInfo; + created_at?: string; + updated_at?: string; } export interface MCPTool { diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index 9484ee32..3d0c3ee3 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -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 { - return this.post('/api/v1/mcp/servers', { source: server }); + public createMCPServer(server: MCPServer): Promise { + return this.post('/api/v1/mcp/servers', server); } public updateMCPServer( serverName: string, - server: Partial, + server: Partial, ): Promise { return this.put(`/api/v1/mcp/servers/${serverName}`, server); } diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 44c0ce95..873461b5 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -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', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 1907689e..fd8cd418 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -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タイムアウトは数値である必要があります', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 8cbac761..9dcfecd3 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -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: '参数', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 6c654b91..08749bc6 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -270,7 +270,7 @@ const zhHant = { }, mcp: { title: 'MCP管理', - createServer: '建立MCP伺服器', + createServer: '新增MCP伺服器', editServer: '編輯MCP伺服器', deleteServer: '刪除MCP伺服器', confirmDeleteServer: '您確定要刪除此MCP伺服器嗎?',