Kaynağa Gözat

feat(auth): Implement JWT authentication for Novel Platform MCP

Frontend changes:
- Add login modal dialog with username/password inputs
- Implement TokenManager for JWT token storage (localStorage)
- Add platformLogin function to call REST API
- Display MCP status: 🔒 (needs login), ✅ username (logged in), 🔓 (public)
- Update MCP config with correct base_url and login_api paths

Backend changes:
- Add /api/auth/login endpoint - proxy to Novel Platform user login
- Add /api/auth/admin-login endpoint - proxy to Novel Platform admin login
- Implement Token injection in mcp_client.py (Authorization header)
- Update conversation_manager.py to pass tokens to tool handler
- Update tool_handler.py to use token per MCP server
- Update tool_converter.py to preserve _server_id metadata

Features:
- Click on 🔒 MCP opens login dialog
- Login calls local backend which proxies to Novel Platform API (no CORS)
- Token stored in localStorage and automatically added to MCP requests
- Login status displayed next to MCP name
- Logout functionality included

This fixes the architecture where:
- ❌ WRONG: Login via MCP tool
- ✅ CORRECT: Login via REST API, then use token in MCP requests

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>]
Claude AI 12 saat önce
ebeveyn
işleme
13e55d0638

+ 82 - 34
backend/app_fastapi.py

@@ -19,6 +19,8 @@ from anthropic import Anthropic
 from config import MCP_SERVERS, ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, ANTHROPIC_MODEL
 from config import MCP_SERVERS, ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, ANTHROPIC_MODEL
 from conversation_manager import ConversationManager
 from conversation_manager import ConversationManager
 from tool_handler import ToolCallHandler
 from tool_handler import ToolCallHandler
+from tool_converter import ToolConverter
+from mcp_client import MCPClient
 
 
 
 
 # 存储认证会话 (生产环境应使用 Redis 或数据库)
 # 存储认证会话 (生产环境应使用 Redis 或数据库)
@@ -108,22 +110,37 @@ async def health():
 async def chat(request: Request):
 async def chat(request: Request):
     """
     """
     聊天端点 - 接收用户消息,返回 Claude 响应(支持 MCP 工具调用)
     聊天端点 - 接收用户消息,返回 Claude 响应(支持 MCP 工具调用)
+
+    支持 MCP 认证:通过 X-MCP-Tokens header 传递 JWT tokens
     """
     """
     try:
     try:
         data = await request.json()
         data = await request.json()
         message = data.get('message', '')
         message = data.get('message', '')
         conversation_history = data.get('history', [])
         conversation_history = data.get('history', [])
         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)
 
 
         if not message:
         if not message:
             raise HTTPException(status_code=400, detail="Message is required")
             raise HTTPException(status_code=400, detail="Message is required")
 
 
-        # 创建对话管理器
+        # 解析 MCP tokens
+        parsed_tokens = {}
+        if mcp_tokens:
+            if isinstance(mcp_tokens, str):
+                try:
+                    parsed_tokens = json_module.loads(mcp_tokens)
+                except:
+                    parsed_tokens = {}
+            else:
+                parsed_tokens = mcp_tokens
+
+        # 创建对话管理器(带 token)
         conv_manager = ConversationManager(
         conv_manager = ConversationManager(
             api_key=ANTHROPIC_API_KEY,
             api_key=ANTHROPIC_API_KEY,
             base_url=ANTHROPIC_BASE_URL,
             base_url=ANTHROPIC_BASE_URL,
             model=ANTHROPIC_MODEL,
             model=ANTHROPIC_MODEL,
-            session_id=session_id
+            session_id=session_id,
+            mcp_tokens=parsed_tokens
         )
         )
 
 
         # 格式化对话历史
         # 格式化对话历史
@@ -163,19 +180,32 @@ async def chat(request: Request):
 async def generate_chat_stream(
 async def generate_chat_stream(
     message: str,
     message: str,
     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
 ):
 ):
     """生成 SSE 流式响应的异步生成器"""
     """生成 SSE 流式响应的异步生成器"""
     try:
     try:
         # 发送开始事件
         # 发送开始事件
         yield f"event: start\ndata: {json_module.dumps({'status': 'started'})}\n\n"
         yield f"event: start\ndata: {json_module.dumps({'status': 'started'})}\n\n"
 
 
-        # 创建对话管理器
+        # 解析 MCP tokens (从 JSON 字符串)
+        parsed_tokens = {}
+        if mcp_tokens:
+            if isinstance(mcp_tokens, str):
+                try:
+                    parsed_tokens = json_module.loads(mcp_tokens)
+                except:
+                    parsed_tokens = {}
+            else:
+                parsed_tokens = mcp_tokens
+
+        # 创建对话管理器(带 token)
         conv_manager = ConversationManager(
         conv_manager = ConversationManager(
             api_key=ANTHROPIC_API_KEY,
             api_key=ANTHROPIC_API_KEY,
             base_url=ANTHROPIC_BASE_URL,
             base_url=ANTHROPIC_BASE_URL,
             model=ANTHROPIC_MODEL,
             model=ANTHROPIC_MODEL,
-            session_id=session_id
+            session_id=session_id,
+            mcp_tokens=parsed_tokens
         )
         )
 
 
         # 格式化对话历史
         # 格式化对话历史
@@ -349,18 +379,21 @@ async def chat_stream(request: Request):
     1. Claude 的思考过程
     1. Claude 的思考过程
     2. 工具调用状态
     2. 工具调用状态
     3. 最终响应
     3. 最终响应
+
+    支持 MCP 认证:通过 X-MCP-Tokens header 传递 JWT tokens
     """
     """
     try:
     try:
         data = await request.json()
         data = await request.json()
         message = data.get('message', '')
         message = data.get('message', '')
         conversation_history = data.get('history', [])
         conversation_history = data.get('history', [])
         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)
 
 
         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),
+            generate_chat_stream(message, conversation_history, session_id, mcp_tokens),
             media_type="text/event-stream",
             media_type="text/event-stream",
             headers={
             headers={
                 'Cache-Control': 'no-cache',
                 'Cache-Control': 'no-cache',
@@ -399,15 +432,30 @@ async def list_mcp_servers():
 
 
 
 
 @app.get("/api/mcp/tools")
 @app.get("/api/mcp/tools")
-async def list_mcp_tools(x_session_id: Optional[str] = Header(None, alias='X-Session-ID')):
-    """获取可用的 MCP 工具列表"""
+async def list_mcp_tools(
+    x_session_id: Optional[str] = Header(None, alias='X-Session-ID'),
+    x_mcp_tokens: Optional[str] = Header(None, alias='X-MCP-Tokens')
+):
+    """获取可用的 MCP 工具列表(支持带 token 的认证)"""
     try:
     try:
-        # 使用静态方法获取工具
-        tools = ConversationManager.get_tools(session_id=x_session_id)
+        # 解析 MCP tokens
+        parsed_tokens = {}
+        if x_mcp_tokens:
+            try:
+                parsed_tokens = json_module.loads(x_mcp_tokens)
+            except:
+                parsed_tokens = {}
+
+        # 使用带 token 的方法获取工具
+        tools = await MCPClient.get_all_tools_with_tokens_async(
+            session_id=x_session_id,
+            mcp_tokens=parsed_tokens
+        )
+        claude_tools = ToolConverter.convert_mcp_tools(tools)
 
 
         return {
         return {
-            "tools": tools,
-            "count": len(tools)
+            "tools": claude_tools,
+            "count": len(claude_tools)
         }
         }
     except Exception as e:
     except Exception as e:
         import traceback
         import traceback
@@ -431,32 +479,28 @@ async def login(request: Request):
     """
     """
     try:
     try:
         data = await request.json()
         data = await request.json()
-        username = data.get('username')
+        # 支持 email 和 username 两种参数名
+        email = data.get('email') or data.get('username')
         password = data.get('password')
         password = data.get('password')
 
 
-        if not username or not password:
-            raise HTTPException(status_code=400, detail="Username and password are required")
-
-        # 查找需要 JWT 认证的 MCP 服务器
-        target_server = None
-        for server_id, config in MCP_SERVERS.items():
-            if config.get('auth_type') == 'jwt' and 'login_url' in config:
-                target_server = config
-                break
+        if not email or not password:
+            raise HTTPException(status_code=400, detail="Email and password are required")
 
 
+        # 查找用户 MCP 服务器
+        target_server = MCP_SERVERS.get('novel-platform-user')
         if not target_server:
         if not target_server:
-            raise HTTPException(status_code=400, detail="No JWT-authenticated server configured")
+            raise HTTPException(status_code=400, detail="Novel Platform User server not configured")
 
 
         # 构建登录 URL
         # 构建登录 URL
         base_url = target_server.get('base_url', '')
         base_url = target_server.get('base_url', '')
-        login_path = target_server.get('login_url', '/api/auth/login')
+        login_path = target_server.get('login_url', '/api/v1/auth/login')
         login_url = f"{base_url}{login_path}"
         login_url = f"{base_url}{login_path}"
 
 
         # 调用实际的登录接口(异步版本)
         # 调用实际的登录接口(异步版本)
         async with httpx.AsyncClient(timeout=30.0) as http_client:
         async with httpx.AsyncClient(timeout=30.0) as http_client:
             response = await http_client.post(
             response = await http_client.post(
                 login_url,
                 login_url,
-                json={"username": username, "password": password}
+                json={"email": email, "password": password}
             )
             )
 
 
         if response.status_code == 200:
         if response.status_code == 200:
@@ -465,7 +509,8 @@ async def login(request: Request):
 
 
             # 存储会话信息
             # 存储会话信息
             auth_sessions[session_id] = {
             auth_sessions[session_id] = {
-                "username": username,
+                "username": result.get("username", email),
+                "email": email,
                 "token": result.get("token"),
                 "token": result.get("token"),
                 "refresh_token": result.get("refresh_token"),
                 "refresh_token": result.get("refresh_token"),
                 "server": target_server.get("name")
                 "server": target_server.get("name")
@@ -474,7 +519,7 @@ async def login(request: Request):
             return {
             return {
                 "success": True,
                 "success": True,
                 "session_id": session_id,
                 "session_id": session_id,
-                "username": username,
+                "username": result.get("username", email),
                 "server": target_server.get("name"),
                 "server": target_server.get("name"),
                 "token": result.get("token")
                 "token": result.get("token")
             }
             }
@@ -494,14 +539,16 @@ async def login(request: Request):
 async def admin_login(request: Request):
 async def admin_login(request: Request):
     """
     """
     Novel Platform 管理员登录
     Novel Platform 管理员登录
+    代理到实际的管理员登录端点并返回 JWT Token
     """
     """
     try:
     try:
         data = await request.json()
         data = await request.json()
-        username = data.get('username')
+        # 支持 email 和 username 两种参数名
+        email = data.get('email') or data.get('username')
         password = data.get('password')
         password = data.get('password')
 
 
-        if not username or not password:
-            raise HTTPException(status_code=400, detail="Username and password are required")
+        if not email or not password:
+            raise HTTPException(status_code=400, detail="Email and password are required")
 
 
         # 查找管理员 MCP 服务器
         # 查找管理员 MCP 服务器
         target_server = MCP_SERVERS.get('novel-platform-admin')
         target_server = MCP_SERVERS.get('novel-platform-admin')
@@ -510,14 +557,14 @@ async def admin_login(request: Request):
 
 
         # 构建登录 URL
         # 构建登录 URL
         base_url = target_server.get('base_url', '')
         base_url = target_server.get('base_url', '')
-        login_path = target_server.get('login_url', '/api/auth/admin-login')
+        login_path = target_server.get('login_url', '/api/v1/auth/admin-login')
         login_url = f"{base_url}{login_path}"
         login_url = f"{base_url}{login_path}"
 
 
         # 调用实际的登录接口(异步版本)
         # 调用实际的登录接口(异步版本)
         async with httpx.AsyncClient(timeout=30.0) as http_client:
         async with httpx.AsyncClient(timeout=30.0) as http_client:
             response = await http_client.post(
             response = await http_client.post(
                 login_url,
                 login_url,
-                json={"username": username, "password": password}
+                json={"email": email, "password": password}
             )
             )
 
 
         if response.status_code == 200:
         if response.status_code == 200:
@@ -525,7 +572,8 @@ async def admin_login(request: Request):
             session_id = str(uuid.uuid4())
             session_id = str(uuid.uuid4())
 
 
             auth_sessions[session_id] = {
             auth_sessions[session_id] = {
-                "username": username,
+                "username": result.get("username", email),
+                "email": email,
                 "token": result.get("token"),
                 "token": result.get("token"),
                 "refresh_token": result.get("refresh_token"),
                 "refresh_token": result.get("refresh_token"),
                 "server": target_server.get("name"),
                 "server": target_server.get("name"),
@@ -535,7 +583,7 @@ async def admin_login(request: Request):
             return {
             return {
                 "success": True,
                 "success": True,
                 "session_id": session_id,
                 "session_id": session_id,
-                "username": username,
+                "username": result.get("username", email),
                 "server": target_server.get("name"),
                 "server": target_server.get("name"),
                 "role": "admin",
                 "role": "admin",
                 "token": result.get("token")
                 "token": result.get("token")

+ 21 - 6
backend/conversation_manager.py

@@ -33,14 +33,17 @@ class ConversationManager:
         api_key: str,
         api_key: str,
         base_url: str,
         base_url: str,
         model: str,
         model: str,
-        session_id: str = None
+        session_id: str = None,
+        mcp_tokens: dict = None
     ):
     ):
         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.tool_handler = ToolCallHandler(session_id=session_id)
+        self.mcp_tokens = mcp_tokens or {}  # MCP 服务器 token 映射
+        self.tool_handler = ToolCallHandler(session_id=session_id, mcp_tokens=mcp_tokens)
         self._cached_tools = None
         self._cached_tools = None
+        self._tool_to_server_map = {}  # 工具名到服务器 ID 的映射
         # 使用自定义 client,支持 Bearer token 认证
         # 使用自定义 client,支持 Bearer token 认证
         self.client = create_anthropic_client(api_key, base_url)
         self.client = create_anthropic_client(api_key, base_url)
 
 
@@ -49,11 +52,22 @@ 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 服务器发现工具
-        mcp_tools = await MCPClient.get_all_tools_async(self.session_id)
+        # 从 MCP 服务器发现工具(带 token)
+        mcp_tools = await MCPClient.get_all_tools_with_tokens_async(
+            self.session_id, self.mcp_tokens
+        )
 
 
         # 转换为 Claude 格式
         # 转换为 Claude 格式
-        claude_tools = ToolConverter.convert_mcp_tools(mcp_tools)
+        claude_tools = []
+        for tool in mcp_tools:
+            claude_tool = ToolConverter.mcp_to_claude_tool(tool)
+            claude_tools.append(claude_tool)
+
+            # 构建工具名到服务器 ID 的映射
+            server_id = tool.get("_server_id", "")
+            if server_id:
+                self._tool_to_server_map[claude_tool["name"]] = server_id
+
         self._cached_tools = claude_tools
         self._cached_tools = claude_tools
 
 
         return claude_tools
         return claude_tools
@@ -168,7 +182,8 @@ class ConversationManager:
 
 
             # 处理工具调用
             # 处理工具调用
             tool_results = await self.tool_handler.process_tool_use_blocks(
             tool_results = await self.tool_handler.process_tool_use_blocks(
-                tool_use_blocks
+                tool_use_blocks,
+                self._tool_to_server_map
             )
             )
 
 
             # 记录工具调用
             # 记录工具调用

+ 38 - 1
backend/mcp_client.py

@@ -35,10 +35,11 @@ def parse_sse_response(text: str) -> str:
 class MCPClient:
 class MCPClient:
     """MCP 客户端,负责工具发现和调用"""
     """MCP 客户端,负责工具发现和调用"""
 
 
-    def __init__(self, server_id: str = None, session_id: str = None):
+    def __init__(self, server_id: str = None, session_id: str = None, auth_token: str = None):
         self.server_id = server_id or "novel-translator"
         self.server_id = server_id or "novel-translator"
         self.server_config = MCP_SERVERS.get(self.server_id, {})
         self.server_config = MCP_SERVERS.get(self.server_id, {})
         self.session_id = session_id
         self.session_id = session_id
+        self.auth_token = auth_token  # JWT token for authenticated MCPs
         self.base_url = self.server_config.get("url", "")
         self.base_url = self.server_config.get("url", "")
 
 
     async def discover_tools(self) -> List[Dict[str, Any]]:
     async def discover_tools(self) -> List[Dict[str, Any]]:
@@ -57,6 +58,10 @@ class MCPClient:
                 if self.session_id:
                 if self.session_id:
                     headers["X-Session-ID"] = self.session_id
                     headers["X-Session-ID"] = self.session_id
 
 
+                # 添加 JWT token 认证
+                if self.auth_token:
+                    headers["Authorization"] = f"Bearer {self.auth_token}"
+
                 payload = {
                 payload = {
                     "jsonrpc": "2.0",
                     "jsonrpc": "2.0",
                     "id": 1,
                     "id": 1,
@@ -97,6 +102,10 @@ class MCPClient:
                 if self.session_id:
                 if self.session_id:
                     headers["X-Session-ID"] = self.session_id
                     headers["X-Session-ID"] = self.session_id
 
 
+                # 添加 JWT token 认证
+                if self.auth_token:
+                    headers["Authorization"] = f"Bearer {self.auth_token}"
+
                 payload = {
                 payload = {
                     "jsonrpc": "2.0",
                     "jsonrpc": "2.0",
                     "id": 2,
                     "id": 2,
@@ -156,6 +165,34 @@ class MCPClient:
 
 
         return all_tools
         return all_tools
 
 
+    @staticmethod
+    async def get_all_tools_with_tokens_async(
+        session_id: str = None,
+        mcp_tokens: dict = None
+    ) -> List[Dict[str, Any]]:
+        """获取所有已配置 MCP 服务器的工具列表(带 token 认证)"""
+        all_tools = []
+
+        for server_id in MCP_SERVERS.keys():
+            if not MCP_SERVERS[server_id].get("enabled", False):
+                continue
+
+            # 获取该服务器的 token
+            auth_token = None
+            if mcp_tokens and server_id in mcp_tokens:
+                auth_token = mcp_tokens[server_id]
+
+            client = MCPClient(server_id, session_id, auth_token)
+            try:
+                tools = await client.discover_tools()
+                for tool in tools:
+                    tool["_server_id"] = server_id
+                    all_tools.append(tool)
+            except Exception as e:
+                print(f"发现 {server_id} 工具失败: {e}")
+
+        return all_tools
+
     @staticmethod
     @staticmethod
     def get_all_tools(session_id: str = None) -> List[Dict[str, Any]]:
     def get_all_tools(session_id: str = None) -> List[Dict[str, Any]]:
         """获取所有已配置 MCP 服务器的工具列表(同步版本)"""
         """获取所有已配置 MCP 服务器的工具列表(同步版本)"""

+ 5 - 0
backend/tool_converter.py

@@ -22,6 +22,7 @@ class ToolConverter:
         name = mcp_tool.get("name", "")
         name = mcp_tool.get("name", "")
         description = mcp_tool.get("description", "")
         description = mcp_tool.get("description", "")
         input_schema = mcp_tool.get("inputSchema", {})
         input_schema = mcp_tool.get("inputSchema", {})
+        server_id = mcp_tool.get("_server_id", "")  # 获取服务器 ID
 
 
         # 构建 Claude 工具格式
         # 构建 Claude 工具格式
         claude_tool = {
         claude_tool = {
@@ -34,6 +35,10 @@ class ToolConverter:
             }
             }
         }
         }
 
 
+        # 存储服务器 ID 元数据(用于后续调用时查找)
+        if server_id:
+            claude_tool["_server_id"] = server_id
+
         # 转换 inputSchema
         # 转换 inputSchema
         if isinstance(input_schema, dict):
         if isinstance(input_schema, dict):
             properties = input_schema.get("properties", {})
             properties = input_schema.get("properties", {})

+ 23 - 7
backend/tool_handler.py

@@ -11,12 +11,14 @@ from tool_converter import ToolConverter
 class ToolCallHandler:
 class ToolCallHandler:
     """处理 Claude 返回的 tool_use 类型内容块"""
     """处理 Claude 返回的 tool_use 类型内容块"""
 
 
-    def __init__(self, session_id: str = None):
+    def __init__(self, session_id: str = None, mcp_tokens: dict = None):
         self.session_id = session_id
         self.session_id = session_id
+        self.mcp_tokens = mcp_tokens or {}  # MCP 服务器 token 映射
 
 
     async def process_tool_use_block(
     async def process_tool_use_block(
         self,
         self,
-        tool_use_block: Dict[str, Any]
+        tool_use_block: Dict[str, Any],
+        tool_to_server_map: dict = None
     ) -> Dict[str, Any]:
     ) -> Dict[str, Any]:
         """
         """
         处理单个 tool_use 内容块
         处理单个 tool_use 内容块
@@ -29,6 +31,7 @@ class ToolCallHandler:
                     "name": "tool_name",
                     "name": "tool_name",
                     "input": {...}
                     "input": {...}
                 }
                 }
+            tool_to_server_map: 工具名到服务器 ID 的映射
 
 
         Returns:
         Returns:
             工具执行结果
             工具执行结果
@@ -43,25 +46,38 @@ class ToolCallHandler:
                 "error": "工具名称为空"
                 "error": "工具名称为空"
             }
             }
 
 
-        # 调用 MCP 工具
-        client = MCPClient(session_id=self.session_id)
+        # 查找工具所属的服务器
+        server_id = None
+        if tool_to_server_map and tool_name in tool_to_server_map:
+            server_id = tool_to_server_map[tool_name]
+
+        # 获取该服务器的 token
+        auth_token = None
+        if server_id and server_id in self.mcp_tokens:
+            auth_token = self.mcp_tokens[server_id]
+
+        # 调用 MCP 工具(带 token)
+        client = MCPClient(session_id=self.session_id, server_id=server_id, auth_token=auth_token)
         result = await client.call_tool(tool_name, tool_input)
         result = await client.call_tool(tool_name, tool_input)
 
 
         return {
         return {
             "tool_use_id": tool_id,
             "tool_use_id": tool_id,
             "tool_name": tool_name,
             "tool_name": tool_name,
-            "result": result
+            "result": result,
+            "server_id": server_id
         }
         }
 
 
     async def process_tool_use_blocks(
     async def process_tool_use_blocks(
         self,
         self,
-        content_blocks: List[Dict[str, Any]]
+        content_blocks: List[Dict[str, Any]],
+        tool_to_server_map: dict = None
     ) -> List[Dict[str, Any]]:
     ) -> List[Dict[str, Any]]:
         """
         """
         批量处理 tool_use 内容块
         批量处理 tool_use 内容块
 
 
         Args:
         Args:
             content_blocks: Claude 返回的内容块列表
             content_blocks: Claude 返回的内容块列表
+            tool_to_server_map: 工具名到服务器 ID 的映射
 
 
         Returns:
         Returns:
             工具执行结果列表
             工具执行结果列表
@@ -76,7 +92,7 @@ class ToolCallHandler:
 
 
         # 并发执行所有工具调用
         # 并发执行所有工具调用
         tasks = [
         tasks = [
-            self.process_tool_use_block(block)
+            self.process_tool_use_block(block, tool_to_server_map)
             for block in tool_use_blocks
             for block in tool_use_blocks
         ]
         ]
 
 

+ 286 - 9
frontend/index.html

@@ -457,6 +457,32 @@
         </div>
         </div>
     </div>
     </div>
 
 
+    <!-- 登录对话框 -->
+    <div id="login-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
+        <div class="bg-white rounded-lg p-6 max-w-md w-full mx-4 shadow-2xl">
+            <div class="flex items-center justify-between mb-4">
+                <h3 class="text-xl font-bold text-gray-800" id="login-title">登录 Novel Platform</h3>
+                <button id="login-close-x" class="text-gray-500 hover:text-gray-700 text-2xl">&times;</button>
+            </div>
+            <p id="login-description" class="text-sm text-gray-600 mb-4">请输入您的账号密码以访问需要认证的 MCP 工具</p>
+            <form id="login-form">
+                <div class="mb-3">
+                    <label for="login-username" class="block text-sm font-medium text-gray-700 mb-1">用户名/邮箱</label>
+                    <input type="text" id="login-username" placeholder="请输入用户名或邮箱" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent">
+                </div>
+                <div class="mb-3">
+                    <label for="login-password" class="block text-sm font-medium text-gray-700 mb-1">密码</label>
+                    <input type="password" id="login-password" placeholder="请输入密码" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent">
+                </div>
+                <p id="login-error" class="text-red-500 text-sm mb-3 hidden"></p>
+                <div class="flex justify-end gap-2">
+                    <button type="button" id="login-cancel" class="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">取消</button>
+                    <button type="submit" id="login-submit" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">登录</button>
+                </div>
+            </form>
+        </div>
+    </div>
+
     <script>
     <script>
         // State
         // State
         let conversationHistory = [];
         let conversationHistory = [];
@@ -473,7 +499,203 @@
         // API Base URL
         // API Base URL
         const API_BASE = window.location.origin;
         const API_BASE = window.location.origin;
 
 
-        // Format message content (handle code blocks, etc.)
+        // ========== MCP 服务器配置 ==========
+        const MCP_SERVERS = {
+            "novel-translator": {
+                "name": "Novel Translator MCP",
+                "auth_type": "none"
+            },
+            "novel-platform-user": {
+                "name": "Novel Platform User MCP",
+                "url": "https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun/mcp/",
+                "auth_type": "jwt",
+                "login_api": "/api/v1/auth/login",
+                "base_url": "https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun"
+            },
+            "novel-platform-admin": {
+                "name": "Novel Platform Admin MCP",
+                "url": "https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun/mcp/",
+                "auth_type": "jwt",
+                "login_api": "/api/v1/auth/admin-login",
+                "base_url": "https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun"
+            }
+        };
+
+        // ========== Token 管理器 ==========
+        const TokenManager = {
+            // 保存 token
+            saveToken(mcpType, token, username = null) {
+                localStorage.setItem(`mcp_token_${mcpType}`, token);
+                localStorage.setItem(`mcp_token_${mcpType}_time`, Date.now());
+                if (username) {
+                    localStorage.setItem(`mcp_username_${mcpType}`, username);
+                }
+            },
+
+            // 获取 token
+            getToken(mcpType) {
+                return localStorage.getItem(`mcp_token_${mcpType}`);
+            },
+
+            // 获取用户名
+            getUsername(mcpType) {
+                return localStorage.getItem(`mcp_username_${mcpType}`);
+            },
+
+            // 清除 token
+            clearToken(mcpType) {
+                localStorage.removeItem(`mcp_token_${mcpType}`);
+                localStorage.removeItem(`mcp_token_${mcpType}_time`);
+                localStorage.removeItem(`mcp_username_${mcpType}`);
+            },
+
+            // 检查是否已登录
+            isLoggedIn(mcpType) {
+                return !!this.getToken(mcpType);
+            },
+
+            // 获取 token 存储时长(毫秒)
+            getTokenAge(mcpType) {
+                const time = localStorage.getItem(`mcp_token_${mcpType}_time`);
+                return time ? (Date.now() - parseInt(time)) : null;
+            }
+        };
+
+        // ========== 登录相关 ==========
+        let currentLoginMcpType = null;
+        const loginModal = document.getElementById('login-modal');
+        const loginForm = document.getElementById('login-form');
+        const loginUsername = document.getElementById('login-username');
+        const loginPassword = document.getElementById('login-password');
+        const loginError = document.getElementById('login-error');
+        const loginTitle = document.getElementById('login-title');
+        const loginSubmit = document.getElementById('login-submit');
+
+        // 显示登录对话框
+        function showLoginModal(mcpType) {
+            currentLoginMcpType = mcpType;
+            const config = MCP_SERVERS[mcpType];
+            if (!config) return;
+
+            loginTitle.textContent = `登录 ${config.name}`;
+            loginUsername.value = '';
+            loginPassword.value = '';
+            loginError.classList.add('hidden');
+            loginSubmit.disabled = false;
+            loginModal.classList.remove('hidden');
+            loginUsername.focus();
+        }
+
+        // 隐藏登录对话框
+        function hideLoginModal() {
+            loginModal.classList.add('hidden');
+            currentLoginMcpType = null;
+        }
+
+        // 执行登录 - 使用本地后端代理避免 CORS 问题
+        async function performLogin(mcpType, username, password) {
+            const config = MCP_SERVERS[mcpType];
+            if (!config || config.auth_type !== 'jwt') {
+                return { success: false, error: '该 MCP 不需要登录' };
+            }
+
+            // 使用本地后端代理端点,避免直接调用外部 API 导致 CORS 错误
+            const is_admin = mcpType === 'novel-platform-admin';
+            const proxyUrl = is_admin ? '/api/auth/admin-login' : '/api/auth/login';
+
+            try {
+                const response = await fetch(proxyUrl, {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify({
+                        email: username,
+                        password: password
+                    })
+                });
+
+                const data = await response.json();
+
+                if (response.ok && data.success) {
+                    const token = data.token;
+                    TokenManager.saveToken(mcpType, token, data.username || username);
+                    return { success: true, token, username: data.username || username };
+                } else {
+                    return { success: false, error: data.detail || data.error || '登录失败' };
+                }
+            } catch (e) {
+                return { success: false, error: e.message };
+            }
+        }
+
+        // 登录表单提交处理
+        loginForm.addEventListener('submit', async (e) => {
+            e.preventDefault();
+            if (!currentLoginMcpType) return;
+
+            const username = loginUsername.value.trim();
+            const password = loginPassword.value;
+
+            if (!username || !password) {
+                loginError.textContent = '请输入用户名和密码';
+                loginError.classList.remove('hidden');
+                return;
+            }
+
+            loginSubmit.disabled = true;
+            loginError.classList.add('hidden');
+
+            const result = await performLogin(currentLoginMcpType, username, password);
+
+            if (result.success) {
+                hideLoginModal();
+                loadMCPServers(); // 刷新 MCP 服务器列表
+            } else {
+                loginError.textContent = result.error;
+                loginError.classList.remove('hidden');
+                loginSubmit.disabled = false;
+            }
+        });
+
+        // 登录对话框事件绑定
+        document.getElementById('login-cancel').addEventListener('click', hideLoginModal);
+        document.getElementById('login-close-x').addEventListener('click', hideLoginModal);
+
+        // 点击背景关闭
+        loginModal.addEventListener('click', (e) => {
+            if (e.target === loginModal) {
+                hideLoginModal();
+            }
+        });
+
+        // ========== Token 传递到后端 ==========
+        // 为每个请求添加 Authorization header
+        async function fetchWithAuth(url, options = {}) {
+            // 检查 URL 路径,确定是哪个 MCP
+            let mcpType = null;
+            if (url.includes('/chat') || url.includes('/mcp')) {
+                // 对于需要认证的 MCP,检查本地存储的 token
+                for (const [type, config] of Object.entries(MCP_SERVERS)) {
+                    if (config.auth_type === 'jwt' && TokenManager.isLoggedIn(type)) {
+                        mcpType = type;
+                        break;
+                    }
+                }
+            }
+
+            // 添加 token 到请求头
+            if (mcpType) {
+                const token = TokenManager.getToken(mcpType);
+                if (token) {
+                    options.headers = options.headers || {};
+                    options.headers['X-MCP-Token'] = token;
+                    options.headers['X-MCP-Type'] = mcpType;
+                }
+            }
+
+            return fetch(url, options);
+        }
+
+        // ========== Format message content (handle code blocks, etc.) ==========
         function formatMessage(content) {
         function formatMessage(content) {
             // Simple markdown-like formatting
             // Simple markdown-like formatting
             let formatted = content
             let formatted = content
@@ -715,9 +937,23 @@
             const events = [];
             const events = [];
 
 
             try {
             try {
+                // 收集所有已登录 MCP 的 tokens
+                const mcpTokens = {};
+                for (const [type, config] of Object.entries(MCP_SERVERS)) {
+                    if (config.auth_type === 'jwt' && TokenManager.isLoggedIn(type)) {
+                        mcpTokens[type] = TokenManager.getToken(type);
+                    }
+                }
+
+                const headers = { 'Content-Type': 'application/json' };
+                // 将 tokens 作为 JSON 字符串传递
+                if (Object.keys(mcpTokens).length > 0) {
+                    headers['X-MCP-Tokens'] = JSON.stringify(mcpTokens);
+                }
+
                 const response = await fetch('/api/chat/stream', {
                 const response = await fetch('/api/chat/stream', {
                     method: 'POST',
                     method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
+                    headers: headers,
                     body: JSON.stringify({ message, history: conversationHistory })
                     body: JSON.stringify({ message, history: conversationHistory })
                 });
                 });
 
 
@@ -949,18 +1185,59 @@
                 const response = await fetch(`${API_BASE}/api/mcp/servers`);
                 const response = await fetch(`${API_BASE}/api/mcp/servers`);
                 const data = await response.json();
                 const data = await response.json();
 
 
-                mcpServers.innerHTML = data.servers.map(server => `
-                    <div class="mcp-server-tag ${server.enabled ? 'enabled' : 'disabled'}">
-                        <span class="mcp-status-dot ${server.enabled ? 'online' : 'offline'}"></span>
-                        <span class="font-medium">${server.name}</span>
-                        ${server.auth_type === 'jwt' ? '<span>🔒</span>' : ''}
-                    </div>
-                `).join('');
+                mcpServers.innerHTML = data.servers.map(server => {
+                    const mcpType = server.id;
+                    const isLoggedIn = TokenManager.isLoggedIn(mcpType);
+                    const username = TokenManager.getUsername(mcpType);
+
+                    let authStatus = '';
+                    let clickHandler = '';
+
+                    if (server.auth_type === 'jwt') {
+                        if (isLoggedIn) {
+                            authStatus = `<span class="text-green-600 font-medium" title="已登录为 ${username}">✅ ${username || '已登录'}</span>`;
+                            clickHandler = `onclick="handleMcpClick('${mcpType}', '${server.auth_type}')"`;
+                        } else {
+                            authStatus = '<span class="text-amber-600" title="需要登录">🔒 需要登录</span>';
+                            clickHandler = `onclick="handleMcpClick('${mcpType}', '${server.auth_type}')"`;
+                        }
+                    } else {
+                        authStatus = '<span class="text-gray-500">🔓 公开</span>';
+                    }
+
+                    return `
+                        <div class="mcp-server-tag ${server.enabled ? 'enabled' : 'disabled'}" ${clickHandler} style="cursor: ${server.auth_type === 'jwt' ? 'pointer' : 'default'}">
+                            <span class="mcp-status-dot ${server.enabled ? 'online' : 'offline'}"></span>
+                            <span class="font-medium">${server.name}</span>
+                            ${authStatus}
+                        </div>
+                    `;
+                }).join('');
             } catch (error) {
             } catch (error) {
                 console.error('Failed to load MCP servers:', error);
                 console.error('Failed to load MCP servers:', error);
             }
             }
         }
         }
 
 
+        // 处理 MCP 服务器点击
+        function handleMcpClick(mcpType, authType) {
+            if (authType !== 'jwt') return;
+
+            if (TokenManager.isLoggedIn(mcpType)) {
+                // 已登录,询问是否登出
+                const username = TokenManager.getUsername(mcpType);
+                if (confirm(`当前已登录为 ${username}\n\n是否要登出?`)) {
+                    TokenManager.clearToken(mcpType);
+                    loadMCPServers();
+                }
+            } else {
+                // 未登录,显示登录对话框
+                showLoginModal(mcpType);
+            }
+        }
+
+        // 将 handleMcpClick 暴露到全局作用域
+        window.handleMcpClick = handleMcpClick;
+
         // Check health
         // Check health
         async function checkHealth() {
         async function checkHealth() {
             try {
             try {