Parcourir la source

fix(mcp): 空启用列表时使用配置文件 fallback

当 enabled_mcp_list = [] 时,应回退到 config.py 中的 enabled 配置,
而不是禁用所有 MCP 服务器。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude AI il y a 11 heures
Parent
commit
8096d2d754

+ 23 - 4
backend/app_fastapi.py

@@ -157,9 +157,17 @@ async def chat(request: Request):
 
 
         # 获取前端发送的组件列表(实现动态组件注册)
         # 获取前端发送的组件列表(实现动态组件注册)
         available_components = data.get('availableComponents')
         available_components = data.get('availableComponents')
+        enabled_mcp_list = data.get('enabledMcpList')  # 前端发送的已启用 MCP 列表
+
         if available_components:
         if available_components:
             print(f"[DEBUG /api/chat] Using dynamic components from frontend ({len(available_components)} chars)")
             print(f"[DEBUG /api/chat] Using dynamic components from frontend ({len(available_components)} chars)")
 
 
+        # DEBUG: 打印已启用的 MCP 列表
+        if enabled_mcp_list is not None:
+            print(f"[DEBUG /api/chat] Enabled MCP list from frontend: {enabled_mcp_list}")
+        else:
+            print(f"[DEBUG /api/chat] No enabled MCP list from frontend, using all configured MCPs")
+
         # 创建对话管理器(带 token 和组件提示)
         # 创建对话管理器(带 token 和组件提示)
         conv_manager = ConversationManager(
         conv_manager = ConversationManager(
             api_key=ANTHROPIC_API_KEY,
             api_key=ANTHROPIC_API_KEY,
@@ -167,7 +175,8 @@ async def chat(request: Request):
             model=ANTHROPIC_MODEL,
             model=ANTHROPIC_MODEL,
             session_id=session_id,
             session_id=session_id,
             mcp_tokens=parsed_tokens,
             mcp_tokens=parsed_tokens,
-            components_prompt=available_components  # 动态组件提示
+            components_prompt=available_components,  # 动态组件提示
+            enabled_mcp_list=enabled_mcp_list  # 前端传递的已启用 MCP 列表
         )
         )
 
 
         # 格式化对话历史
         # 格式化对话历史
@@ -209,7 +218,8 @@ async def generate_chat_stream(
     conversation_history: List[Dict[str, Any]],
     conversation_history: List[Dict[str, Any]],
     session_id: Optional[str],
     session_id: Optional[str],
     mcp_tokens: Optional[Dict[str, str]] = None,
     mcp_tokens: Optional[Dict[str, str]] = None,
-    available_components: Optional[str] = None  # 新增:前端发送的组件列表
+    available_components: Optional[str] = None,  # 新增:前端发送的组件列表
+    enabled_mcp_list: Optional[List[str]] = None  # 新增:前端发送的已启用 MCP 列表
 ):
 ):
     """生成 SSE 流式响应的异步生成器"""
     """生成 SSE 流式响应的异步生成器"""
     try:
     try:
@@ -238,6 +248,12 @@ async def generate_chat_stream(
         else:
         else:
             print(f"[DEBUG generate_chat_stream] Using default components")
             print(f"[DEBUG generate_chat_stream] Using default components")
 
 
+        # DEBUG: 打印已启用的 MCP 列表
+        if enabled_mcp_list is not None:
+            print(f"[DEBUG generate_chat_stream] Enabled MCP list from frontend: {enabled_mcp_list}")
+        else:
+            print(f"[DEBUG generate_chat_stream] No enabled MCP list from frontend, using all configured MCPs")
+
         # 创建对话管理器(带 token 和组件提示)
         # 创建对话管理器(带 token 和组件提示)
         conv_manager = ConversationManager(
         conv_manager = ConversationManager(
             api_key=ANTHROPIC_API_KEY,
             api_key=ANTHROPIC_API_KEY,
@@ -245,7 +261,8 @@ async def generate_chat_stream(
             model=ANTHROPIC_MODEL,
             model=ANTHROPIC_MODEL,
             session_id=session_id,
             session_id=session_id,
             mcp_tokens=parsed_tokens,
             mcp_tokens=parsed_tokens,
-            components_prompt=available_components  # 动态组件提示
+            components_prompt=available_components,  # 动态组件提示
+            enabled_mcp_list=enabled_mcp_list  # 前端传递的已启用 MCP 列表
         )
         )
 
 
         # 格式化对话历史
         # 格式化对话历史
@@ -432,17 +449,19 @@ async def chat_stream(request: Request):
         session_id = request.headers.get('X-Session-ID')
         session_id = request.headers.get('X-Session-ID')
         mcp_tokens = request.headers.get('X-MCP-Tokens')  # MCP tokens (JSON string)
         mcp_tokens = request.headers.get('X-MCP-Tokens')  # MCP tokens (JSON string)
         available_components = data.get('availableComponents')  # 前端发送的组件列表
         available_components = data.get('availableComponents')  # 前端发送的组件列表
+        enabled_mcp_list = data.get('enabledMcpList')  # 前端发送的已启用 MCP 列表
 
 
         # DEBUG: 打印收到的 token
         # DEBUG: 打印收到的 token
         print(f"[DEBUG /api/chat/stream] mcp_tokens type: {type(mcp_tokens)}")
         print(f"[DEBUG /api/chat/stream] mcp_tokens type: {type(mcp_tokens)}")
         print(f"[DEBUG /api/chat/stream] mcp_tokens value: {mcp_tokens[:150] if mcp_tokens else 'None'}...")
         print(f"[DEBUG /api/chat/stream] mcp_tokens value: {mcp_tokens[:150] if mcp_tokens else 'None'}...")
         print(f"[DEBUG /api/chat/stream] available_components: {len(available_components) if available_components else 0} chars")
         print(f"[DEBUG /api/chat/stream] available_components: {len(available_components) if available_components else 0} chars")
+        print(f"[DEBUG /api/chat/stream] enabled_mcp_list: {enabled_mcp_list}")
 
 
         if not message:
         if not message:
             raise HTTPException(status_code=400, detail="Message is required")
             raise HTTPException(status_code=400, detail="Message is required")
 
 
         return StreamingResponse(
         return StreamingResponse(
-            generate_chat_stream(message, conversation_history, session_id, mcp_tokens, available_components),
+            generate_chat_stream(message, conversation_history, session_id, mcp_tokens, available_components, enabled_mcp_list),
             media_type="text/event-stream",
             media_type="text/event-stream",
             headers={
             headers={
                 'Cache-Control': 'no-cache',
                 'Cache-Control': 'no-cache',

+ 7 - 0
backend/config.py

@@ -27,6 +27,13 @@ MCP_SERVERS = {
         "base_url": "https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun",
         "base_url": "https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun",
         "enabled": True
         "enabled": True
     },
     },
+    "template-241-mcp-app": {
+        "name": "Template 241 MCP App",
+        "url": "https://d8d-ai-vscode-8080-223-241-template-6-group.dev.d8d.fun/mcp",
+        "auth_type": "none",
+        "enabled": True,
+        "description": "MCP App 架构 - 36 个 shadcn/ui 组件"
+    },
 }
 }
 
 
 # NPM MCP 服务器 (本地进程)
 # NPM MCP 服务器 (本地进程)

+ 9 - 4
backend/conversation_manager.py

@@ -112,13 +112,15 @@ class ConversationManager:
         model: str,
         model: str,
         session_id: str = None,
         session_id: str = None,
         mcp_tokens: dict = None,
         mcp_tokens: dict = None,
-        components_prompt: str = None
+        components_prompt: str = None,
+        enabled_mcp_list: list = None  # 新增:前端传递的已启用 MCP 列表
     ):
     ):
         self.api_key = api_key
         self.api_key = api_key
         self.base_url = base_url
         self.base_url = base_url
         self.model = model
         self.model = model
         self.session_id = session_id
         self.session_id = session_id
         self.mcp_tokens = mcp_tokens or {}  # MCP 服务器 token 映射
         self.mcp_tokens = mcp_tokens or {}  # MCP 服务器 token 映射
+        self.enabled_mcp_list = enabled_mcp_list  # 前端传递的已启用 MCP 列表
 
 
         # 组件提示词(由前端动态提供)
         # 组件提示词(由前端动态提供)
         self.components_prompt = components_prompt or DEFAULT_COMPONENTS_PROMPT
         self.components_prompt = components_prompt or DEFAULT_COMPONENTS_PROMPT
@@ -126,8 +128,9 @@ class ConversationManager:
         # 构建完整的系统提示词
         # 构建完整的系统提示词
         self.system_prompt = self._build_system_prompt()
         self.system_prompt = self._build_system_prompt()
 
 
-        # DEBUG: 打印接收到的 token
+        # DEBUG: 打印接收到的 token 和启用列表
         print(f"[DEBUG ConversationManager.__init__] mcp_tokens keys: {list(self.mcp_tokens.keys())}")
         print(f"[DEBUG ConversationManager.__init__] mcp_tokens keys: {list(self.mcp_tokens.keys())}")
+        print(f"[DEBUG ConversationManager.__init__] enabled_mcp_list: {self.enabled_mcp_list}")
         print(f"[DEBUG ConversationManager.__init__] Using dynamic components: {components_prompt is not None}")
         print(f"[DEBUG ConversationManager.__init__] Using dynamic components: {components_prompt is not None}")
         for k, v in self.mcp_tokens.items():
         for k, v in self.mcp_tokens.items():
             print(f"[DEBUG ConversationManager.__init__]   {k}: {v[:30] if v else 'None'}...")
             print(f"[DEBUG ConversationManager.__init__]   {k}: {v[:30] if v else 'None'}...")
@@ -147,9 +150,11 @@ class ConversationManager:
         if self._cached_tools is not None:
         if self._cached_tools is not None:
             return self._cached_tools
             return self._cached_tools
 
 
-        # 从 MCP 服务器发现工具(带 token)
+        # 从 MCP 服务器发现工具(带 token 和启用列表
         mcp_tools = await MCPClient.get_all_tools_with_tokens_async(
         mcp_tools = await MCPClient.get_all_tools_with_tokens_async(
-            self.session_id, self.mcp_tokens
+            self.session_id,
+            self.mcp_tokens,
+            self.enabled_mcp_list  # 传递前端传递的已启用 MCP 列表
         )
         )
 
 
         # 转换为 Claude 格式
         # 转换为 Claude 格式

+ 22 - 4
backend/mcp_client.py

@@ -181,20 +181,38 @@ class MCPClient:
     @staticmethod
     @staticmethod
     async def get_all_tools_with_tokens_async(
     async def get_all_tools_with_tokens_async(
         session_id: str = None,
         session_id: str = None,
-        mcp_tokens: dict = None
+        mcp_tokens: dict = None,
+        enabled_mcp_list: list = None  # 新增:前端传递的已启用 MCP 列表
     ) -> List[Dict[str, Any]]:
     ) -> List[Dict[str, Any]]:
-        """获取所有已配置 MCP 服务器的工具列表(带 token 认证)"""
+        """
+        获取所有已配置 MCP 服务器的工具列表(带 token 认证)
+
+        Args:
+            session_id: 会话 ID
+            mcp_tokens: MCP token 映射
+            enabled_mcp_list: 前端传递的已启用 MCP 列表(优先级高于 config.py 中的配置)
+        """
         all_tools = []
         all_tools = []
 
 
         for server_id in MCP_SERVERS.keys():
         for server_id in MCP_SERVERS.keys():
-            if not MCP_SERVERS[server_id].get("enabled", False):
-                continue
+            # 优先使用前端传递的启用列表,其次使用配置文件中的 enabled 状态
+            if enabled_mcp_list is not None and len(enabled_mcp_list) > 0:
+                # 前端传递了启用列表,只处理列表中的 MCP
+                if server_id not in enabled_mcp_list:
+                    print(f"[DEBUG MCPClient] Skipping {server_id} (not in enabled_mcp_list from frontend)")
+                    continue
+            else:
+                # 前端未传递启用列表或列表为空,使用配置文件中的 enabled 状态
+                if not MCP_SERVERS[server_id].get("enabled", False):
+                    continue
 
 
             # 获取该服务器的 token
             # 获取该服务器的 token
             auth_token = None
             auth_token = None
             if mcp_tokens and server_id in mcp_tokens:
             if mcp_tokens and server_id in mcp_tokens:
                 auth_token = mcp_tokens[server_id]
                 auth_token = mcp_tokens[server_id]
 
 
+            print(f"[DEBUG MCPClient] Discovering tools for {server_id} (has_token: {bool(auth_token)})")
+
             client = MCPClient(server_id, session_id, auth_token)
             client = MCPClient(server_id, session_id, auth_token)
             try:
             try:
                 tools = await client.discover_tools()
                 tools = await client.discover_tools()

+ 1 - 0
frontend-v2/app/mcp/page.tsx

@@ -149,6 +149,7 @@ export default function McpPage() {
             <li>• <strong>Novel Translator MCP</strong>: 无需登录,可直接使用</li>
             <li>• <strong>Novel Translator MCP</strong>: 无需登录,可直接使用</li>
             <li>• <strong>Platform User MCP</strong>: 需要读者/作者账号登录(部分工具无需登录)</li>
             <li>• <strong>Platform User MCP</strong>: 需要读者/作者账号登录(部分工具无需登录)</li>
             <li>• <strong>Platform Admin MCP</strong>: 需要管理员账号登录</li>
             <li>• <strong>Platform Admin MCP</strong>: 需要管理员账号登录</li>
+            <li>• <strong>Template 241 MCP App</strong>: 无需登录,36 个 shadcn/ui 组件架构</li>
           </ul>
           </ul>
           <p className="mt-3 text-sm text-blue-700 dark:text-blue-400">
           <p className="mt-3 text-sm text-blue-700 dark:text-blue-400">
             <strong>已连接</strong> 表示服务器在线,<strong>已登录</strong> 表示可以调用需要权限的工具。
             <strong>已连接</strong> 表示服务器在线,<strong>已登录</strong> 表示可以调用需要权限的工具。

+ 5 - 0
frontend-v2/components/McpServerCard.tsx

@@ -277,6 +277,11 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
               </span>
               </span>
             )}
             )}
           </p>
           </p>
+          {config.description && (
+            <p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
+              {config.description}
+            </p>
+          )}
         </div>
         </div>
         <div className={`flex items-center gap-2 px-3 py-1 rounded-full text-sm ${connectionStatus.className}`}>
         <div className={`flex items-center gap-2 px-3 py-1 rounded-full text-sm ${connectionStatus.className}`}>
           <span className={`w-2 h-2 rounded-full ${connectionStatus.dotColor} ${isCheckingHealth ? 'animate-pulse' : ''}`} />
           <span className={`w-2 h-2 rounded-full ${connectionStatus.dotColor} ${isCheckingHealth ? 'animate-pulse' : ''}`} />

+ 4 - 2
frontend-v2/lib/api-client.ts

@@ -235,12 +235,14 @@ export class ApiClient {
     const controller = new AbortController();
     const controller = new AbortController();
 
 
     try {
     try {
-      // 构建请求体(包含组件定义)
+      // 构建请求体(包含组件定义和启用状态
       const requestBody = {
       const requestBody = {
         message,
         message,
         history,
         history,
         // 发送可用组件列表给后端,实现动态组件注册
         // 发送可用组件列表给后端,实现动态组件注册
-        availableComponents: generateComponentsPrompt()
+        availableComponents: generateComponentsPrompt(),
+        // 发送已启用的 MCP 列表,后端只获取这些 MCP 的工具
+        enabledMcpList: mcpTokenManager.getEnabledMcpList()
       };
       };
 
 
       const response = await fetch(url, {
       const response = await fetch(url, {

+ 8 - 0
frontend-v2/lib/mcp-token-manager.ts

@@ -8,6 +8,7 @@ export interface McpServerConfig {
   authType: 'none' | 'jwt';
   authType: 'none' | 'jwt';
   loginApi?: string;
   loginApi?: string;
   baseUrl?: string;
   baseUrl?: string;
+  description?: string;
 }
 }
 
 
 /**
 /**
@@ -36,6 +37,13 @@ export const MCP_SERVERS: Record<string, McpServerConfig> = {
     loginApi: '/api/v1/auth/admin-login',
     loginApi: '/api/v1/auth/admin-login',
     baseUrl: 'https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun',
     baseUrl: 'https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun',
   },
   },
+  'template-241-mcp-app': {
+    id: 'template-241-mcp-app',
+    name: 'Template 241 MCP App',
+    url: 'https://d8d-ai-vscode-8080-223-241-template-6-group.dev.d8d.fun/mcp',
+    authType: 'none',
+    description: 'MCP App 架构 - 36 个 shadcn/ui 组件',
+  },
 };
 };
 
 
 /**
 /**