2
0
Просмотр исходного кода

fix(mcp): 修复 MCP 启用/禁用状态同步和 Hydration 错误

1. 统一 McpServerCard 使用 mcpTokenManager.isEnabled() 读取状态
2. 修复 toggleEnabled/setEnabled 方法的 localStorage 存储逻辑
3. 修复 Header 从 MCP_SERVERS 获取正确的总数(4 个)
4. 添加 mounted 状态模式避免 React Hydration 错误
5. 所有条件渲染添加 mounted 检查确保 SSR/CSR 一致性
6. 后端系统提示词根据 enabledMcpList 动态生成

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude AI 19 часов назад
Родитель
Сommit
a2da92cecc

+ 54 - 4
backend/conversation_manager.py

@@ -29,9 +29,13 @@ DEFAULT_COMPONENTS_PROMPT = """### 可用的 json-render 组件
 
 
 
 
 # ========== 基础 System Prompt ==========
 # ========== 基础 System Prompt ==========
-# 不包含组件列表,组件列表由前端动态提供
+# 不包含组件列表和 MCP 工具说明,由 _build_system_prompt 动态构建
 BASE_SYSTEM_PROMPT = """你是一个 AI 助手,可以通过调用 MCP 工具来帮助用户完成任务。
 BASE_SYSTEM_PROMPT = """你是一个 AI 助手,可以通过调用 MCP 工具来帮助用户完成任务。
 
 
+## 当前状态
+
+{MCP_STATUS}
+
 ## 重要:你可以返回 UI 组件
 ## 重要:你可以返回 UI 组件
 
 
 除了普通文本,你还可以返回 **json-render 组件 spec** 来展示更丰富的 UI。组件 spec 是一个 JSON 对象,前端会自动渲染成 UI 组件。
 除了普通文本,你还可以返回 **json-render 组件 spec** 来展示更丰富的 UI。组件 spec 是一个 JSON 对象,前端会自动渲染成 UI 组件。
@@ -61,7 +65,11 @@ BASE_SYSTEM_PROMPT = """你是一个 AI 助手,可以通过调用 MCP 工具
    - 用户查看列表后,建议"下一页"、"筛选"等操作
    - 用户查看列表后,建议"下一页"、"筛选"等操作
    - 使用 `button` 或 `suggestion-buttons` 组件
    - 使用 `button` 或 `suggestion-buttons` 组件
 
 
-### 小说相关操作(重要)
+{MCP_TOOLS_GUIDE}
+"""
+
+# 小说相关操作说明(只有启用相关 MCP 时才显示)
+NOVEL_TOOLS_GUIDE = """### 小说相关操作(重要)
 
 
 当用户说"查看小说:xxx"、"小说详情:xxx"或点击小说卡片时:
 当用户说"查看小说:xxx"、"小说详情:xxx"或点击小说卡片时:
 1. 从消息中提取小说标题或 ID
 1. 从消息中提取小说标题或 ID
@@ -86,6 +94,16 @@ BASE_SYSTEM_PROMPT = """你是一个 AI 助手,可以通过调用 MCP 工具
 ```
 ```
 """
 """
 
 
+# 无 MCP 工具时的提示
+NO_TOOLS_GUIDE = """### 注意
+
+当前没有启用任何 MCP 服务器,因此你没有可用的工具。你只能:
+- 进行普通对话
+- 返回 json-render 组件来展示信息
+
+如果用户询问关于翻译、小说等功能,请告知用户需要先在 MCP 管理页面启用相关服务器。
+"""
+
 
 
 def create_anthropic_client(api_key: str, base_url: str) -> Anthropic:
 def create_anthropic_client(api_key: str, base_url: str) -> Anthropic:
     """
     """
@@ -142,8 +160,40 @@ class ConversationManager:
         self.client = create_anthropic_client(api_key, base_url)
         self.client = create_anthropic_client(api_key, base_url)
 
 
     def _build_system_prompt(self) -> str:
     def _build_system_prompt(self) -> str:
-        """构建完整的系统提示词(基础提示 + 组件列表)"""
-        return BASE_SYSTEM_PROMPT + "\n\n" + self.components_prompt
+        """构建完整的系统提示词(基础提示 + MCP 状态 + 工具指南 + 组件列表)"""
+        # 根据 enabled_mcp_list 构建 MCP 状态提示和工具指南
+        if self.enabled_mcp_list is not None and len(self.enabled_mcp_list) == 0:
+            # 所有 MCP 都被禁用
+            mcp_status = """**重要:当前没有启用任何 MCP 服务器**
+
+你没有可用的 MCP 工具。如果用户询问关于翻译、小说、组件等功能,请告诉用户:
+- 当前没有启用任何 MCP 服务器
+- 请前往 MCP 管理页面启用需要的服务器
+- 启用后刷新页面即可使用相关功能
+
+你仍然可以使用基础的对话能力和返回 json-render 组件来帮助用户。"""
+            mcp_tools_guide = NO_TOOLS_GUIDE
+        elif self.enabled_mcp_list is not None and len(self.enabled_mcp_list) > 0:
+            # 部分或全部 MCP 已启用
+            enabled_names = ", ".join(self.enabled_mcp_list)
+            mcp_status = f"""**已启用的 MCP 服务器**: {enabled_names}
+
+你可以调用这些 MCP 服务器的工具来帮助用户完成任务。"""
+            # 检查是否有小说相关的 MCP 启用
+            has_novel_mcp = any('novel-platform' in mcp or 'novel' in mcp for mcp in self.enabled_mcp_list)
+            if has_novel_mcp:
+                mcp_tools_guide = NOVEL_TOOLS_GUIDE
+            else:
+                mcp_tools_guide = ""
+        else:
+            # enabled_mcp_list 是 None,使用配置文件的默认状态
+            mcp_status = """你可以通过调用 MCP 工具来帮助用户完成任务。"""
+            mcp_tools_guide = NOVEL_TOOLS_GUIDE  # 默认显示小说工具指南
+
+        # 替换占位符并添加组件列表
+        prompt = BASE_SYSTEM_PROMPT.replace("{MCP_STATUS}", mcp_status)
+        prompt = prompt.replace("{MCP_TOOLS_GUIDE}", mcp_tools_guide)
+        return prompt + "\n\n" + self.components_prompt
 
 
     async def get_available_tools(self) -> List[Dict[str, Any]]:
     async def get_available_tools(self) -> List[Dict[str, Any]]:
         """获取可用的 Claude 格式工具列表(带缓存)"""
         """获取可用的 Claude 格式工具列表(带缓存)"""

+ 35 - 12
frontend-v2/components/Header.tsx

@@ -13,28 +13,51 @@ import Link from 'next/link';
 import { MCP_SERVERS, mcpTokenManager } from '@/lib/mcp-token-manager';
 import { MCP_SERVERS, mcpTokenManager } from '@/lib/mcp-token-manager';
 
 
 export default function Header() {
 export default function Header() {
-  const [loggedInCount, setLoggedInCount] = useState(0);
+  // 客户端挂载标志 - 用于避免 hydration 错误
+  const [mounted, setMounted] = useState(false);
+  // 初始值设为 0,避免 SSR/CSR 不匹配
   const [enabledCount, setEnabledCount] = useState(0);
   const [enabledCount, setEnabledCount] = useState(0);
+  const [loggedInCount, setLoggedInCount] = useState(0);
   // 从 MCP_SERVERS 配置获取总数
   // 从 MCP_SERVERS 配置获取总数
   const total = Object.keys(MCP_SERVERS).length;
   const total = Object.keys(MCP_SERVERS).length;
 
 
   useEffect(() => {
   useEffect(() => {
-    // 更新 MCP 登录状态
+    // 设置客户端挂载标志
+    setMounted(true);
+
+    // 从 localStorage 读取真实状态(只在客户端执行)
     const updateStatus = () => {
     const updateStatus = () => {
-      setLoggedInCount(mcpTokenManager.getLoggedInMcpList().length);
-      setEnabledCount(mcpTokenManager.getEnabledMcpList().length);
+      const enabled = mcpTokenManager.getEnabledMcpList().length;
+      const loggedIn = mcpTokenManager.getLoggedInMcpList().length;
+      console.log('[Header] updateStatus:', { enabled, loggedIn });
+      setEnabledCount(enabled);
+      setLoggedInCount(loggedIn);
     };
     };
 
 
+    // 初始化时更新状态
     updateStatus();
     updateStatus();
 
 
-    // 监听 storage 变化
+    // 监听 storage 变化(其他标签页修改 localStorage)
     const handleStorageChange = () => updateStatus();
     const handleStorageChange = () => updateStatus();
     window.addEventListener('storage', handleStorageChange);
     window.addEventListener('storage', handleStorageChange);
-    return () => window.removeEventListener('storage', handleStorageChange);
-  }, []);
+
+    // 自定义事件:MCP 启用状态变化(当前页面内)
+    const handleMcpEnabledChange = () => updateStatus();
+    window.addEventListener('mcp-enabled-change', handleMcpEnabledChange);
+
+    // 监听 focus 事件,确保页面获得焦点时更新状态
+    const handleFocus = () => updateStatus();
+    window.addEventListener('focus', handleFocus);
+
+    return () => {
+      window.removeEventListener('storage', handleStorageChange);
+      window.removeEventListener('mcp-enabled-change', handleMcpEnabledChange);
+      window.removeEventListener('focus', handleFocus);
+    };
+  }, []); // 空依赖数组,只在挂载时执行一次
 
 
   return (
   return (
-    <header className="bg-white dark:bg-gray-800 border-b dark:border-gray-700 px-3 py-2 md:px-6 md:py-4 safe-area-inset-top">
+    <header className="bg-white dark:bg-gray-800 border-b dark:border-gray-700 px-3 py-2 md:px-6 md:py-4 safe-area-inset-top" suppressHydrationWarning>
       {/* 移动端:两行布局 */}
       {/* 移动端:两行布局 */}
       <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2 md:gap-0">
       <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2 md:gap-0">
         {/* 第一行:标题和导航 */}
         {/* 第一行:标题和导航 */}
@@ -58,13 +81,13 @@ export default function Header() {
           <div className="flex items-center space-x-1 md:space-x-2 text-xs md:text-sm">
           <div className="flex items-center space-x-1 md:space-x-2 text-xs md:text-sm">
             <span className="text-gray-600 dark:text-gray-400">MCP:</span>
             <span className="text-gray-600 dark:text-gray-400">MCP:</span>
             <span className={`px-1.5 md:px-2 py-0.5 md:py-1 rounded ${
             <span className={`px-1.5 md:px-2 py-0.5 md:py-1 rounded ${
-              enabledCount > 0
+              mounted && enabledCount > 0
                 ? 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200'
                 ? 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200'
                 : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
                 : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
             }`}>
             }`}>
-              已启用 {enabledCount}/{total}
+              {mounted ? `已启用 ${enabledCount}/${total}` : '加载中...'}
             </span>
             </span>
-            {loggedInCount > 0 && (
+            {mounted && loggedInCount > 0 && (
               <span className="hidden sm:inline-block px-1.5 md:px-2 py-0.5 md:py-1 rounded bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
               <span className="hidden sm:inline-block px-1.5 md:px-2 py-0.5 md:py-1 rounded bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
                 已登录 {loggedInCount}/{total}
                 已登录 {loggedInCount}/{total}
               </span>
               </span>
@@ -75,7 +98,7 @@ export default function Header() {
             href="/mcp"
             href="/mcp"
             className="text-xs md:text-sm px-2 md:px-4 py-1.5 md:py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
             className="text-xs md:text-sm px-2 md:px-4 py-1.5 md:py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
           >
           >
-            {loggedInCount > 0 ? '管理' : '连接 MCP'}
+            {mounted ? (loggedInCount > 0 ? '管理' : '连接 MCP') : 'MCP'}
           </Link>
           </Link>
         </div>
         </div>
       </div>
       </div>

+ 33 - 21
frontend-v2/components/McpServerCard.tsx

@@ -89,23 +89,24 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
   const [isLoading, setIsLoading] = useState(false);
   const [isLoading, setIsLoading] = useState(false);
   const [error, setError] = useState('');
   const [error, setError] = useState('');
   const [updateTrigger, setUpdateTrigger] = useState(0);
   const [updateTrigger, setUpdateTrigger] = useState(0);
-  // 新增:客户端挂载标志
+  // 客户端挂载标志 - 用于避免 hydration 错误
   const [mounted, setMounted] = useState(false);
   const [mounted, setMounted] = useState(false);
-  // 新增:健康检查状态
-  const [isHealthy, setIsHealthy] = useState<boolean | null>(null); // null=未检查, true=健康, false=不健康
+  // 启用/禁用状态 - 初始值统一为 true,客户端挂载后从 localStorage 读取真实值
+  const [isEnabled, setIsEnabled] = useState(true);
+  // 健康检查状态
+  const [isHealthy, setIsHealthy] = useState<boolean | null>(null);
   const [isCheckingHealth, setIsCheckingHealth] = useState(false);
   const [isCheckingHealth, setIsCheckingHealth] = useState(false);
   const [latency, setLatency] = useState<number | null>(null);
   const [latency, setLatency] = useState<number | null>(null);
-  // 新增:启用/禁用状态 - 初始值设为 true 避免闪烁,useEffect 中会更新真实值
-  const [isEnabled, setIsEnabled] = useState(true);
 
 
   // 监听 localStorage 变化和组件挂载
   // 监听 localStorage 变化和组件挂载
   useEffect(() => {
   useEffect(() => {
     // 设置客户端挂载标志
     // 设置客户端挂载标志
     setMounted(true);
     setMounted(true);
 
 
-    // 初始化启用状态 - 从 localStorage 读取真实值
-    const realEnabledState = mcpTokenManager.isEnabled(mcpType);
-    setIsEnabled(realEnabledState);
+    // 从 localStorage 读取真实的启用状态
+    const storedEnabled = mcpTokenManager.isEnabled(mcpType);
+    console.log(`[McpServerCard ${mcpType}] Client mounted, isEnabled from localStorage: ${storedEnabled}`);
+    setIsEnabled(storedEnabled);
 
 
     // 初始化时检查登录状态
     // 初始化时检查登录状态
     const checkLoginStatus = () => {
     const checkLoginStatus = () => {
@@ -191,11 +192,16 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
   }, [isHealthy, isLoggedIn, mcpType, onConnectionStatusChange]);
   }, [isHealthy, isLoggedIn, mcpType, onConnectionStatusChange]);
 
 
   // 计算连接状态显示
   // 计算连接状态显示
+  // - 未挂载 → 加载中
   // - 禁用 → 灰色圆点
   // - 禁用 → 灰色圆点
   // - 健康 + 已登录 → 已连接 ✓ | 已登录 ✓
   // - 健康 + 已登录 → 已连接 ✓ | 已登录 ✓
   // - 健康 + 未登录 → 已连接 ✓ | 未登录
   // - 健康 + 未登录 → 已连接 ✓ | 未登录
   // - 不健康 → 离线
   // - 不健康 → 离线
   const getConnectionStatus = () => {
   const getConnectionStatus = () => {
+    // 未挂载时显示加载状态,避免 hydration 错误
+    if (!mounted) {
+      return { text: '加载中...', className: 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400', dotColor: 'bg-gray-300' };
+    }
     // 禁用状态优先,显示灰色
     // 禁用状态优先,显示灰色
     if (!isEnabled) {
     if (!isEnabled) {
       return { text: '已禁用', className: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400', dotColor: 'bg-gray-400' };
       return { text: '已禁用', className: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400', dotColor: 'bg-gray-400' };
@@ -255,14 +261,19 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
   };
   };
 
 
   const handleToggleEnabled = () => {
   const handleToggleEnabled = () => {
+    console.log(`[McpServerCard.handleToggleEnabled] Called for ${mcpType}, current isEnabled=${isEnabled}`);
     const newState = mcpTokenManager.toggleEnabled(mcpType);
     const newState = mcpTokenManager.toggleEnabled(mcpType);
+    console.log(`[McpServerCard.handleToggleEnabled] toggleEnabled returned: ${newState}`);
     setIsEnabled(newState);
     setIsEnabled(newState);
     onEnabledChange?.(mcpType, newState);
     onEnabledChange?.(mcpType, newState);
+    // 触发自定义事件,通知 Header 等组件更新状态
+    window.dispatchEvent(new CustomEvent('mcp-enabled-change'));
+    console.log(`[McpServerCard.handleToggleEnabled] Dispatched mcp-enabled-change event`);
   };
   };
 
 
   return (
   return (
     <div
     <div
-      className={`bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 md:p-6 border dark:border-gray-700 transition-all ${!isEnabled ? 'opacity-50 grayscale' : ''}`}
+      className={`bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 md:p-6 border dark:border-gray-700 transition-all ${mounted && !isEnabled ? 'opacity-50 grayscale' : ''}`}
       suppressHydrationWarning
       suppressHydrationWarning
     >
     >
       {/* 头部区域 - 移动端优化 */}
       {/* 头部区域 - 移动端优化 */}
@@ -275,26 +286,27 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
             </h3>
             </h3>
             <p className="text-xs md:text-sm text-gray-500 dark:text-gray-400 mt-0.5">
             <p className="text-xs md:text-sm text-gray-500 dark:text-gray-400 mt-0.5">
               {config.authType === 'none' ? '无需认证' : '需要登录'}
               {config.authType === 'none' ? '无需认证' : '需要登录'}
-              {latency !== null && isHealthy && isEnabled && (
+              {mounted && latency !== null && isHealthy && isEnabled && (
                 <span className="ml-2 text-xs text-gray-400">
                 <span className="ml-2 text-xs text-gray-400">
                   ({latency}ms)
                   ({latency}ms)
                 </span>
                 </span>
               )}
               )}
             </p>
             </p>
           </div>
           </div>
-          {/* 启用/禁用按钮 - 移动端加大点击区域 */}
+          {/* 启用/禁用按钮 - 移动端加大点击区域,只在客户端显示真实状态 */}
           <button
           <button
             type="button"
             type="button"
             onClick={handleToggleEnabled}
             onClick={handleToggleEnabled}
             className={`flex items-center gap-1.5 px-3 py-2 md:px-2.5 md:py-1 rounded-full text-xs md:text-sm font-medium transition-all min-h-[44px] md:min-h-0 ${
             className={`flex items-center gap-1.5 px-3 py-2 md:px-2.5 md:py-1 rounded-full text-xs md:text-sm font-medium transition-all min-h-[44px] md:min-h-0 ${
+              !mounted ? 'bg-gray-100 dark:bg-gray-700 text-gray-400' :
               isEnabled
               isEnabled
                 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 hover:bg-green-200 dark:hover:bg-green-800'
                 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 hover:bg-green-200 dark:hover:bg-green-800'
                 : 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
                 : 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
             }`}
             }`}
-            title={isEnabled ? '点击禁用此 MCP 服务器' : '点击启用此 MCP 服务器'}
+            title={mounted ? (isEnabled ? '点击禁用此 MCP 服务器' : '点击启用此 MCP 服务器') : '加载中...'}
           >
           >
-            <span className={`w-2 h-2 rounded-full ${isEnabled ? 'bg-green-500' : 'bg-gray-400'}`} />
-            {isEnabled ? '禁用' : '启用'}
+            <span className={`w-2 h-2 rounded-full ${!mounted ? 'bg-gray-300' : isEnabled ? 'bg-green-500' : 'bg-gray-400'}`} />
+            {!mounted ? '...' : isEnabled ? '禁用' : '启用'}
           </button>
           </button>
         </div>
         </div>
 
 
@@ -314,8 +326,8 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
         )}
         )}
       </div>
       </div>
 
 
-      {/* 已登录状态显示 - 移动端优化 */}
-      {isLoggedIn && (
+      {/* 已登录状态显示 - 移动端优化,只在客户端渲染 */}
+      {mounted && isLoggedIn && (
         <div className="mb-4 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
         <div className="mb-4 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
           <p className="text-sm text-green-800 dark:text-green-200">
           <p className="text-sm text-green-800 dark:text-green-200">
             已登录: <span className="font-medium">{username}</span>
             已登录: <span className="font-medium">{username}</span>
@@ -326,8 +338,8 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
         </div>
         </div>
       )}
       )}
 
 
-      {/* 禁用提示 - 移动端优化 */}
-      {!isEnabled && (
+      {/* 禁用提示 - 移动端优化,只在客户端渲染 */}
+      {mounted && !isEnabled && (
         <div className="mb-4 p-3 bg-gray-100 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600">
         <div className="mb-4 p-3 bg-gray-100 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600">
           <p className="text-xs md:text-sm text-gray-600 dark:text-gray-400">
           <p className="text-xs md:text-sm text-gray-600 dark:text-gray-400">
             此 MCP 服务器已禁用。点击右上角的"启用"按钮来启用它。
             此 MCP 服务器已禁用。点击右上角的"启用"按钮来启用它。
@@ -335,7 +347,7 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
         </div>
         </div>
       )}
       )}
 
 
-      {showLoginForm && !isLoggedIn && isEnabled && (
+      {mounted && showLoginForm && !isLoggedIn && isEnabled && (
         <form onSubmit={handleLogin} className="mb-4 space-y-3">
         <form onSubmit={handleLogin} className="mb-4 space-y-3">
           <div>
           <div>
             <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
             <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
@@ -395,7 +407,7 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
         </form>
         </form>
       )}
       )}
 
 
-      {showTokenForm && !isLoggedIn && (
+      {mounted && showTokenForm && !isLoggedIn && (
         <div className="mb-4 space-y-3">
         <div className="mb-4 space-y-3">
           <div>
           <div>
             <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
             <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
@@ -453,7 +465,7 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
           {config.url}
           {config.url}
         </p>
         </p>
         {/* 操作按钮 - 移动端加大点击区域,按钮靠左对齐 */}
         {/* 操作按钮 - 移动端加大点击区域,按钮靠左对齐 */}
-        {config.authType === 'jwt' && isEnabled && (
+        {mounted && config.authType === 'jwt' && isEnabled && (
           <div className="flex gap-3 md:gap-2">
           <div className="flex gap-3 md:gap-2">
             {!isLoggedIn ? (
             {!isLoggedIn ? (
               !showLoginForm && !showTokenForm && (
               !showLoginForm && !showTokenForm && (

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

@@ -323,35 +323,64 @@ export class McpTokenManager {
    * 设置 MCP 服务器启用状态
    * 设置 MCP 服务器启用状态
    */
    */
   setEnabled(mcpType: string, enabled: boolean): void {
   setEnabled(mcpType: string, enabled: boolean): void {
-    if (typeof window === 'undefined') return;
-    localStorage.setItem(`mcp_enabled_${mcpType}`, enabled.toString());
-    console.log(`[McpTokenManager] MCP ${mcpType} ${enabled ? 'enabled' : 'disabled'}`);
+    console.log(`[McpTokenManager.setEnabled] Called with mcpType="${mcpType}", enabled=${enabled}`);
+    if (typeof window === 'undefined') {
+      console.log(`[McpTokenManager.setEnabled] SSR mode, skipping`);
+      return;
+    }
+    const key = `mcp_enabled_${mcpType}`;
+    const value = enabled.toString();
+    localStorage.setItem(key, value);
+    console.log(`[McpTokenManager.setEnabled] Saved: ${key}="${value}"`);
+    // 验证保存是否成功
+    const saved = localStorage.getItem(key);
+    console.log(`[McpTokenManager.setEnabled] Verified: ${key}="${saved}"`);
   }
   }
 
 
   /**
   /**
    * 获取 MCP 服务器启用状态
    * 获取 MCP 服务器启用状态
-   * 默认返回 true(启用)
+   * SSR 时返回 false(避免 hydration 不匹配)
+   * 客户端默认返回 true(启用)
    */
    */
   isEnabled(mcpType: string): boolean {
   isEnabled(mcpType: string): boolean {
-    if (typeof window === 'undefined') return true;
+    if (typeof window === 'undefined') return false;  // SSR 时返回 false
     const value = localStorage.getItem(`mcp_enabled_${mcpType}`);
     const value = localStorage.getItem(`mcp_enabled_${mcpType}`);
-    return value === null ? true : value === 'true';
+    const result = value === null ? true : value === 'true';
+    console.log(`[McpTokenManager.isEnabled] ${mcpType}: value="${value}", result=${result}`);
+    return result;
   }
   }
 
 
   /**
   /**
    * 切换 MCP 服务器启用状态
    * 切换 MCP 服务器启用状态
    */
    */
   toggleEnabled(mcpType: string): boolean {
   toggleEnabled(mcpType: string): boolean {
-    const newState = !this.isEnabled(mcpType);
+    console.log(`[McpTokenManager.toggleEnabled] Called with mcpType="${mcpType}"`);
+    const currentState = this.isEnabled(mcpType);
+    console.log(`[McpTokenManager.toggleEnabled] Current state: ${currentState}`);
+    const newState = !currentState;
+    console.log(`[McpTokenManager.toggleEnabled] New state: ${newState}`);
     this.setEnabled(mcpType, newState);
     this.setEnabled(mcpType, newState);
     return newState;
     return newState;
   }
   }
 
 
   /**
   /**
    * 获取所有已启用的 MCP 列表
    * 获取所有已启用的 MCP 列表
+   * SSR 时返回空数组(避免 hydration 不匹配)
    */
    */
   getEnabledMcpList(): string[] {
   getEnabledMcpList(): string[] {
-    return Object.keys(MCP_SERVERS).filter(mcpType => this.isEnabled(mcpType));
+    if (typeof window === 'undefined') return [];  // SSR 时返回空数组
+    const result: string[] = [];
+    console.log('[McpTokenManager.getEnabledMcpList] Checking all MCPs...');
+    for (const mcpType of Object.keys(MCP_SERVERS)) {
+      const value = localStorage.getItem(`mcp_enabled_${mcpType}`);
+      const enabled = value === null ? true : value === 'true';
+      console.log(`  ${mcpType}: localStorage="${value}", enabled=${enabled}`);
+      if (enabled) {
+        result.push(mcpType);
+      }
+    }
+    console.log(`[McpTokenManager.getEnabledMcpList] Result: [${result.join(', ')}]`);
+    return result;
   }
   }
   /**
   /**
    * 获取已启用且已登录的 MCP 数量
    * 获取已启用且已登录的 MCP 数量