From 2b1900916ba7beedec4c04b472145294ff08396c Mon Sep 17 00:00:00 2001 From: supreme0597 Date: Sat, 17 Jan 2026 01:48:46 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84mcp=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/mcp_router.py | 6 +- server/routers/system_router.py | 2 +- src/services/mcp_service.py | 163 ++++++++----- web/src/components/McpServerDetailModal.vue | 251 +++++++++++++++----- web/src/components/McpServersComponent.vue | 50 +++- 5 files changed, 343 insertions(+), 129 deletions(-) diff --git a/server/routers/mcp_router.py b/server/routers/mcp_router.py index d886ec019..1d9ae2029 100644 --- a/server/routers/mcp_router.py +++ b/server/routers/mcp_router.py @@ -5,6 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from src.services.mcp_service import ( + clear_mcp_server_tools_cache, create_mcp_server, get_mcp_tools_stats, delete_mcp_server, @@ -291,7 +292,7 @@ async def get_mcp_server_tools( } # 提取参数信息 if hasattr(tool, "args_schema") and tool.args_schema: - schema = tool.args_schema.schema() if hasattr(tool.args_schema, "schema") else {} + schema = tool.args_schema tool_info["parameters"] = schema.get("properties", {}) tool_info["required"] = schema.get("required", []) else: @@ -325,6 +326,9 @@ async def refresh_mcp_server_tools( await get_server_or_404(db, name) try: + # 清除缓存,强制刷新 + clear_mcp_server_tools_cache(name) + # 获取所有工具(不过滤 disabled_tools) tools = await get_all_mcp_tools(name) diff --git a/server/routers/system_router.py b/server/routers/system_router.py index 7be72c3d3..7dabc111d 100644 --- a/server/routers/system_router.py +++ b/server/routers/system_router.py @@ -66,7 +66,7 @@ async def get_system_logs(levels: str | None = None, current_user: User = Depend if levels: level_filter = set(level.strip().upper() for level in levels.split(",") if level.strip()) - async with aiofiles.open(LOG_FILE) as f: + async with aiofiles.open(LOG_FILE, encoding="utf-8") as f: # 读取最后1000行 lines = [] async for line in f: diff --git a/src/services/mcp_service.py b/src/services/mcp_service.py index 90651a537..fb7d8da08 100644 --- a/src/services/mcp_service.py +++ b/src/services/mcp_service.py @@ -36,6 +36,15 @@ # MCP Server configurations (Runtime cache, loaded from DB) MCP_SERVERS: dict[str, dict[str, Any]] = {} +# Per-server locks for granular tool fetching (avoids blocking all servers) +_server_locks: dict[str, asyncio.Lock] = {} + +def get_server_lock(name: str) -> asyncio.Lock: + """Get or create a lock for a specific server.""" + if name not in _server_locks: + _server_locks[name] = asyncio.Lock() + return _server_locks[name] + # Default MCP Server configurations (Imported to DB on first run) _DEFAULT_MCP_SERVERS = { "sequentialthinking": { @@ -224,9 +233,9 @@ async def get_mcp_tools( """ global _mcp_tools_cache - # 1. Prepare Server Config + # 1. Prepare Server Config (获取快照后立即释放锁,防止阻塞其他服务器的工具拉取) async with _mcp_lock: - mcp_servers = MCP_SERVERS | (additional_servers or {}) + mcp_servers = (MCP_SERVERS | (additional_servers or {})).copy() all_processed_tools = [] @@ -235,62 +244,68 @@ async def get_mcp_tools( if not force_refresh and cache and server_name in _mcp_tools_cache: all_processed_tools = _mcp_tools_cache[server_name] else: - # Need to fetch from server - try: - assert server_name in mcp_servers, f"Server {server_name} not found in ({list(mcp_servers.keys())})" - - # Extract connection config - server_config = mcp_servers[server_name] - client_config = {k: v for k, v in server_config.items() if k not in ("disabled_tools",)} - - client = await get_mcp_client({server_name: client_config}) - if client is None: + # 针对该服务器加锁,确保同一时间只有一个请求在初始化该服务器 + # 且不阻塞其他服务器 (Server B) 的请求 + async with get_server_lock(server_name): + # 再次检查缓存(双重检查锁定模式) + if not force_refresh and cache and server_name in _mcp_tools_cache: + return _mcp_tools_cache[server_name] + + try: + assert server_name in mcp_servers, f"Server {server_name} not found in ({list(mcp_servers.keys())})" + + # Extract connection config + server_config = mcp_servers[server_name] + client_config = {k: v for k, v in server_config.items() if k not in ("disabled_tools",)} + + client = await get_mcp_client({server_name: client_config}) + if client is None: + return [] + + # Get ALL tools (Raw) + raw_tools = cast(list[Any], await client.get_tools()) + + # Render IDs for ALL tools + server_cc = to_camel_case(server_name) + for tool in raw_tools: + # Render unique ID rule: mcp__[camelCaseServer]__[camelCaseTool] + original_name = tool.name + tool_cc = to_camel_case(original_name) + unique_id = f"mcp__{server_cc}__{tool_cc}" + + # Use metadata to store + if tool.metadata is None: + tool.metadata = {} + tool.metadata["id"] = unique_id + + all_processed_tools.append(tool) + + # Update Cache (Store the FULL list) + if cache: + _mcp_tools_cache[server_name] = all_processed_tools + + # Update Stats + # Stats should reflect the GLOBAL configuration state + # (How many are disabled in the stored config, not the transient arg) + global_config_disabled = mcp_servers.get(server_name, {}).get("disabled_tools") or [] + enabled_count = len([t for t in all_processed_tools if t.name not in global_config_disabled]) + + _mcp_tools_stats[server_name] = { + "total": len(all_processed_tools), + "enabled": enabled_count, + "disabled": len(all_processed_tools) - enabled_count, + } + + logger.info(f"Refreshed MCP tools cache for '{server_name}': {len(all_processed_tools)} tools loaded.") + + except AssertionError as e: + logger.warning(f"[assert] Failed to load tools from MCP server '{server_name}': {e}") + return [] + except Exception as e: + logger.error( + f"Failed to load tools from MCP server '{server_name}': {e}, traceback: {traceback.format_exc()}" + ) return [] - - # Get ALL tools (Raw) - raw_tools = cast(list[Any], await client.get_tools()) - - # Render IDs for ALL tools - server_cc = to_camel_case(server_name) - for tool in raw_tools: - # Render unique ID rule: mcp__[camelCaseServer]__[camelCaseTool] - original_name = tool.name - tool_cc = to_camel_case(original_name) - unique_id = f"mcp__{server_cc}__{tool_cc}" - - # Use metadata to store - if tool.metadata is None: - tool.metadata = {} - tool.metadata["id"] = unique_id - - all_processed_tools.append(tool) - - # Update Cache (Store the FULL list) - if cache: - _mcp_tools_cache[server_name] = all_processed_tools - - # Update Stats - # Stats should reflect the GLOBAL configuration state - # (How many are disabled in the stored config, not the transient arg) - global_config_disabled = mcp_servers.get(server_name, {}).get("disabled_tools") or [] - enabled_count = len([t for t in all_processed_tools if t.name not in global_config_disabled]) - - _mcp_tools_stats[server_name] = { - "total": len(all_processed_tools), - "enabled": enabled_count, - "disabled": len(all_processed_tools) - enabled_count, - } - - logger.info(f"Refreshed MCP tools cache for '{server_name}': {len(all_processed_tools)} tools loaded.") - - except AssertionError as e: - logger.warning(f"[assert] Failed to load tools from MCP server '{server_name}': {e}") - return [] - except Exception as e: - logger.error( - f"Failed to load tools from MCP server '{server_name}': {e}, traceback: {traceback.format_exc()}" - ) - return [] # 3. Filtering (Apply to Return Value Only) if disabled_tools: @@ -540,7 +555,7 @@ async def toggle_tool_enabled( await db.commit() # Clear tool cache (re-filtered on next fetch) - clear_mcp_server_tools_cache(server_name) + # clear_mcp_server_tools_cache(server_name) logger.info(f"Toggled tool '{tool_name}' for server '{server_name}' enabled={enabled}") return enabled, server @@ -598,7 +613,7 @@ async def get_all_mcp_tools(server_name: str) -> list: """Get all tools of an MCP server (no filtering). For management UI to display tool list, supports viewing all tools and their enabled status. - Does NOT update the global tools cache to avoid polluting agent's filtered view. + Works for both enabled and disabled servers. Args: server_name: Server name @@ -606,10 +621,32 @@ async def get_all_mcp_tools(server_name: str) -> list: Returns: List of all tools (unfiltered) """ + # Try to get config from cache first (for enabled servers) config = MCP_SERVERS.get(server_name) + + # If not in cache (disabled server), load from database if not config: - logger.warning(f"MCP server '{server_name}' not found in cache") - return [] + from src.storage.db.manager import db_manager + from src.storage.db.models import MCPServer + + try: + async with db_manager.get_async_session_context() as session: + result = await session.execute( + select(MCPServer).filter(MCPServer.name == server_name) + ) + server = result.scalar_one_or_none() + + if not server: + logger.warning(f"MCP server '{server_name}' not found in database") + return [] + + config = server.to_mcp_config() + logger.info(f"Loaded config for disabled server '{server_name}' from database") + except Exception as e: + logger.error(f"Failed to load server config from database: {e}") + return [] + + # Get all tools (no filtering, use cache by default) + # Pass the config as additional_servers in case it's not in MCP_SERVERS + return await get_mcp_tools(server_name, additional_servers={server_name: config}, disabled_tools=[]) - # Get all tools (no filtering, force refresh, no cache update) - return await get_mcp_tools(server_name, disabled_tools=[], cache=False, force_refresh=True) diff --git a/web/src/components/McpServerDetailModal.vue b/web/src/components/McpServerDetailModal.vue index 631ca69d4..a742031ce 100644 --- a/web/src/components/McpServerDetailModal.vue +++ b/web/src/components/McpServerDetailModal.vue @@ -1,26 +1,26 @@ -
+
{{ server.description }}
-
+
{{ tag }} @@ -112,11 +112,11 @@
- + 刷新工具 @@ -131,8 +131,8 @@
@@ -144,20 +144,28 @@
- +
-
- {{ tool.description }} -
+ +
+ {{ tool.description }} +
+
-
- {{ paramName }} - 必填 + {{ paramName }} + * - {{ param.type || 'any' }}
-
- {{ param.description }} +
+
+ + {{ param.type || 'any' }} +
+ +
+ {{ param.description }} +
+
@@ -214,13 +239,13 @@ diff --git a/web/src/components/AgentConfigSidebar.vue b/web/src/components/AgentConfigSidebar.vue index 28d8e6f62..8f5724cab 100644 --- a/web/src/components/AgentConfigSidebar.vue +++ b/web/src/components/AgentConfigSidebar.vue @@ -118,6 +118,45 @@
+ +
+
+
+ 已选择 {{ getSelectedCount(key) }} 个MCP服务器配置 + + 清空 + +
+ + 选择MCP服务器 + +
+
+ + {{ getMcpNameById(toolId) }} + +
+
+
+ + + +
+ + +
+
+
+
+ {{ mcp.name }} +
+ + +
+
+
{{ mcp.description }}
+
+
+
+ + +
+
@@ -309,7 +402,7 @@ const emit = defineEmits(['close']) // Store 管理 const agentStore = useAgentStore() -const { availableTools, selectedAgent, selectedAgentId, agentConfig, configurableItems } = +const { availableTools, selectedAgent, selectedAgentId, agentConfig, configurableItems, availableMcps } = storeToRefs(agentStore) // console.log(availableTools.value) @@ -543,6 +636,15 @@ const validateAndFilterConfig = () => { if (validatedConfig[key].length !== currentValue.length) { console.warn(`工具配置 ${key} 中包含无效的工具ID,已自动过滤`) } + } else if (configItem.template_metadata?.kind === 'mcps' && Array.isArray(currentValue)) { + const availableMcpIds = availableMcps.value + ? Object.values(availableMcps.value).map((mcp) => mcp.id) + : [] + validatedConfig[key] = currentValue.filter((mcpId) => availableMcpIds.includes(mcpId)) + + if (validatedConfig[key].length !== currentValue.length) { + console.warn(`MCP配置 ${key} 中包含无效的MCP-ID,已自动过滤`) + } } // 检查多选配置项 (type === 'list' 且有 options) @@ -598,6 +700,73 @@ const resetConfig = async () => { message.error('重置配置失败') } } + +const mcpsModalOpen = ref(false) +const selectedMcps = ref([]) +const mcpsSearchText = ref('') + +const filteredMcps = computed(() => { + const mcpsList = availableMcps.value ? Object.values(availableMcps.value) : [] + if (!mcpsSearchText.value) { + return mcpsList + } + const searchLower = mcpsSearchText.value.toLowerCase() + return mcpsList.filter( + (mcp) => + mcp.name.toLowerCase().includes(searchLower) || + mcp.description.toLowerCase().includes(searchLower) + ) +}) + +const getMcpNameById = (mcpId) => { + const mcpsList = availableMcps.value ? Object.values(availableMcps.value) : [] + const mcp = mcpsList.find((t) => t.id === mcpId) + return mcp ? mcp.name : mcpId +} + +const openMcpsModal = async () => { + try { + selectedMcps.value = [...(agentConfig.value?.mcps || [])] + mcpsModalOpen.value = true + } catch (error) { + console.error('打开MCP选择弹窗失败:', error) + message.error('打开MCP选择弹窗失败') + } +} + +const toggleMcpSelection = (mcpId) => { + const index = selectedMcps.value.indexOf(mcpId) + if (index > -1) { + selectedMcps.value.splice(index, 1) + } else { + selectedMcps.value.push(mcpId) + } +} + +const removeSelectedMcp = (mcpId) => { + const currentMcps = [...(agentConfig.value?.mcps || [])] + const index = currentMcps.indexOf(mcpId) + if (index > -1) { + currentMcps.splice(index, 1) + agentStore.updateAgentConfig({ + mcps: currentMcps + }) + } +} + +const confirmMcpsSelection = () => { + agentStore.updateAgentConfig({ + mcps: [...selectedMcps.value] + }) + mcpsModalOpen.value = false + mcpsSearchText.value = '' +} + +const cancelMcpsSelection = () => { + mcpsModalOpen.value = false + mcpsSearchText.value = '' + selectedMcps.value = [] +} \ No newline at end of file diff --git a/web/src/stores/agent.js b/web/src/stores/agent.js index a501fcdb0..4bf3671e4 100644 --- a/web/src/stores/agent.js +++ b/web/src/stores/agent.js @@ -75,6 +75,18 @@ export const useAgentStore = defineStore( return configurableItems.value.tools?.options || [] }) + // MCP相关状态 + const availableMcps = computed(() => { + return configurableItems.value.mcps?.options || [] + }) + const selectedMcpToolsConfig = ref([]) + + // 知识库相关状态 + const availableKnowledges = computed(() => { + return configurableItems.value.knowledges?.options || [] + }) + const selectedKnowledgeToolsConfig = ref([]) + const hasConfigChanges = computed( () => JSON.stringify(agentConfig.value) !== JSON.stringify(originalAgentConfig.value) ) @@ -294,6 +306,20 @@ export const useAgentStore = defineStore( Object.assign(agentConfig.value, updates) } + /** + * 更新mcp 工具配置(支持批量更新) + */ + function updateMcpToolsConfig(newConfig) { + selectedMcpToolsConfig.value = newConfig; + } + + /** + * 更新知识库配置(支持批量更新) + */ + function updateKnowledgeToolsConfig(newConfig) { + selectedKnowledgeToolsConfig.value = newConfig; + } + /** * 清除错误状态 */ @@ -317,6 +343,8 @@ export const useAgentStore = defineStore( error.value = null isInitialized.value = false isInitializing.value = false + selectedMcpToolsConfig.value = [] // 重置 MCP 工具配置 + selectedKnowledgeToolsConfig.value = [] // 重置 知识库 配置 } return { @@ -342,6 +370,12 @@ export const useAgentStore = defineStore( availableTools, hasConfigChanges, + availableMcps, + availableKnowledges, + + selectedMcpToolsConfig, + selectedKnowledgeToolsConfig, + // 方法 initialize, fetchAgents, @@ -355,7 +389,9 @@ export const useAgentStore = defineStore( updateConfigItem, updateAgentConfig, clearError, - reset + reset, + updateMcpToolsConfig, + updateKnowledgeToolsConfig, } }, { @@ -363,7 +399,7 @@ export const useAgentStore = defineStore( persist: { key: 'agent-store', storage: localStorage, - pick: ['selectedAgentId'] + pick: ['selectedAgentId', 'selectedMcpToolsConfig', 'selectedKnowledgeToolsConfig'] } } )