diff --git a/pkg/api/http/controller/groups/mcp.py b/pkg/api/http/controller/groups/mcp.py index 9d85925a..f2abb9da 100644 --- a/pkg/api/http/controller/groups/mcp.py +++ b/pkg/api/http/controller/groups/mcp.py @@ -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/', 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 diff --git a/pkg/core/taskmgr.py b/pkg/core/taskmgr.py index ca6eb029..4eee7104 100644 --- a/pkg/core/taskmgr.py +++ b/pkg/core/taskmgr.py @@ -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, }, } diff --git a/web/src/app/home/plugins/mcp-market/MCPMarketComponent.tsx b/web/src/app/home/plugins/mcp-market/MCPMarketComponent.tsx index a529d67f..d8839678 100644 --- a/web/src/app/home/plugins/mcp-market/MCPMarketComponent.tsx +++ b/web/src/app/home/plugins/mcp-market/MCPMarketComponent.tsx @@ -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; }) { const { t } = useTranslation(); // const [marketServerList, setMarketServerList] = useState( @@ -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({ {/* 已安装的服务器列表 */}

- {t('mcp.installedServers')} + {t('mcp.title')}

{loading ? ( diff --git a/web/src/app/home/plugins/mcp/MCPCardVO.ts b/web/src/app/home/plugins/mcp/MCPCardVO.ts index c35f5508..45cca9ba 100644 --- a/web/src/app/home/plugins/mcp/MCPCardVO.ts +++ b/web/src/app/home/plugins/mcp/MCPCardVO.ts @@ -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; } diff --git a/web/src/app/home/plugins/mcp/mcp-card/MCPCardComponent.tsx b/web/src/app/home/plugins/mcp/mcp-card/MCPCardComponent.tsx index 7a7953e6..c2d1dfcb 100644 --- a/web/src/app/home/plugins/mcp/mcp-card/MCPCardComponent.tsx +++ b/web/src/app/home/plugins/mcp/mcp-card/MCPCardComponent.tsx @@ -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({
- {t('mcp.toolCount', { count: cardVO.tools })} + {t('mcp.toolCount', { count: toolsCount })}
-
+
e.stopPropagation()} + > handleEnable(e)} + onCheckedChange={handleEnable} disabled={!switchEnable} />
diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 5fe06c4c..6d02627c 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -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(null); const mcpComponentRef = useRef(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([]); + const [mcpTestError, setMcpTestError] = useState(''); + + // 缓存每个服务器测试后的工具数量 + const [serverToolsCache, setServerToolsCache] = useState>({}); + // 强制清理 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) { const extraArgsObj: Record = {}; @@ -458,6 +610,7 @@ export default function PluginConfigPage() { url: value.url, headers: extraArgsObj as Record, timeout: value.timeout, + ssereadtimeout: value.ssereadtimeout, }; if (isEditMode && editingServerName) { @@ -782,6 +935,7 @@ export default function PluginConfigPage() { onEditServer={(serverName) => { loadServerForEdit(serverName); }} + toolsCountCache={serverToolsCache} /> @@ -868,10 +1022,10 @@ export default function PluginConfigPage() { > - {t('plugins.confirmDeleteTitle')} + {t('mcp.confirmDeleteTitle')} - {t('plugins.deleteConfirmation')} + {t('mcp.confirmDeleteServer')}
- {t('llm.extraParametersDescription')} + {t('mcp.extraParametersDescription')} diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index 6c106a29..77439691 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -521,8 +521,8 @@ export class BackendClient extends BaseHttpClient { serverName: string, target_enabled: boolean, ): Promise { - return this.put(`/api/v1/mcp/servers/${serverName}/toggle`, { - target_enabled, + return this.put(`/api/v1/mcp/servers/${serverName}`, { + enable: target_enabled, }); } diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index d4932e26..91d49597 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -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: '流水线',