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

$(cat <<'EOF'
feat: 实现完整的 MCP 工具调用管道 - 让 Claude AI 真正调用 MCP 工具

核心功能:
- MCP 工具发现: 从 MCP 服务器获取可用工具列表 (SSE 响应解析)
- 工具转换器: 将 MCP 工具定义转换为 Claude API tools 格式
- tool_use 处理器: 处理 Claude 返回的工具调用请求
- MCP 调用器: 实际调用 MCP 工具并返回结果
- 多轮对话管理: 自动处理工具调用后的后续对话

测试验证:
- ✅ 成功发现 11 个 Novel Translator MCP 工具
- ✅ Claude 智能选择工具并执行翻译任务
- ✅ 自动添加术语表并优化翻译结果

新增文件:
- backend/mcp_client.py: MCP 客户端 + SSE 响应解析
- backend/tool_converter.py: MCP → Claude 工具格式转换
- backend/tool_handler.py: tool_use 响应处理器
- backend/conversation_manager.py: 多轮对话管理器
- backend/app.py: Flask 后端集成完整管道
- frontend/index.html: Web 聊天界面

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
EOF
)

yourname 1 день назад
Родитель
Сommit
bda56c8aa6

+ 6 - 0
.dev-container-config.json

@@ -0,0 +1,6 @@
+{
+  "autoShutdown": {
+    "enabled": false,
+    "idleTimeoutMinutes": 30
+  }
+}

+ 36 - 0
.gitignore

@@ -0,0 +1,36 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+env/
+venv/
+ENV/
+.venv/
+
+# Flask
+instance/
+.webassets-cache
+
+# Node
+node_modules/
+npm-debug.log
+yarn-error.log
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Environment
+.env
+.env.local
+
+# Logs
+*.log

+ 84 - 1
README.md

@@ -1,2 +1,85 @@
-# blank
+# AI MCP Web UI
 
 
+通用 MCP (Model Context Protocol) 服务器 Web 界面,允许用户通过聊天界面与 Claude AI 对话,Claude 可以智能调用 MCP 工具。
+
+## 功能特性
+
+- 🤖 与 Claude AI 进行实时对话
+- 🔌 集成 MCP 服务器工具
+- 💬 现代化的聊天界面
+- 📊 MCP 服务器状态监控
+
+## 项目结构
+
+```
+/mnt/code/223-240-template-6/
+├── backend/           # Python Flask 后端
+│   ├── app.py        # 主应用
+│   ├── config.py     # MCP 服务器配置
+│   └── requirements.txt
+├── frontend/          # 前端界面
+│   └── index.html
+├── .gitignore
+└── README.md
+```
+
+## 快速开始
+
+### 1. 安装依赖
+
+```bash
+# 安装 Python 依赖
+pip install -r backend/requirements.txt
+```
+
+### 2. 配置环境变量
+
+确保设置了以下环境变量:
+- `ANTHROPIC_AUTH_TOKEN`: Claude API 密钥
+- `ANTHROPIC_BASE_URL`: API 基础 URL
+- `ANTHROPIC_MODEL`: 使用的模型名称
+
+### 3. 启动服务
+
+```bash
+cd backend
+python app.py
+```
+
+服务将在 `http://0.0.0.0:5000` 启动。
+
+### 4. 访问界面
+
+打开浏览器访问 `http://localhost:5000`
+
+## MCP 服务器配置
+
+在 `backend/config.py` 中配置 MCP 服务器:
+
+```python
+MCP_SERVERS = {
+    "server-name": {
+        "command": "npx",
+        "args": ["-y", "mcp-server-package"],
+        "enabled": True
+    }
+}
+```
+
+## API 端点
+
+- `GET /` - 前端界面
+- `GET /api/health` - 健康检查
+- `POST /api/chat` - 发送消息
+- `GET /api/mcp/servers` - 获取 MCP 服务器列表
+- `GET /api/mcp/tools` - 获取可用工具列表
+
+## 技术栈
+
+- **后端**: Flask, Anthropic SDK
+- **前端**: HTML, Tailwind CSS, Vanilla JavaScript
+- **协议**: MCP (Model Context Protocol)
+
+## 许可证
+
+MIT License

+ 314 - 0
backend/app.py

@@ -0,0 +1,314 @@
+"""
+AI MCP Web UI - Flask 后端
+提供聊天界面与 MCP 工具调用的桥梁
+"""
+import os
+import asyncio
+from typing import Optional, Dict
+from flask import Flask, request, jsonify, send_from_directory
+from flask_cors import CORS
+import httpx
+from anthropic import Anthropic
+from config import MCP_SERVERS, ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, ANTHROPIC_MODEL
+from conversation_manager import ConversationManager
+
+app = Flask(__name__)
+CORS(app)
+app.secret_key = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
+
+# 存储认证会话 (生产环境应使用 Redis 或数据库)
+auth_sessions: Dict[str, dict] = {}
+
+
+@app.route('/')
+def index():
+    return send_from_directory('../frontend', 'index.html')
+
+
+@app.route('/<path:path>')
+def static_files(path):
+    return send_from_directory('../frontend', path)
+
+
+# 初始化 Claude 客户端
+client = Anthropic(
+    api_key=ANTHROPIC_API_KEY,
+    base_url=ANTHROPIC_BASE_URL
+)
+
+
+@app.route('/api/health', methods=['GET'])
+def health():
+    """健康检查端点"""
+    return jsonify({
+        "status": "ok",
+        "model": ANTHROPIC_MODEL,
+        "mcp_servers": list(MCP_SERVERS.keys())
+    })
+
+
+def run_async(coro):
+    """在同步上下文中运行异步函数"""
+    loop = asyncio.new_event_loop()
+    asyncio.set_event_loop(loop)
+    try:
+        return loop.run_until_complete(coro)
+    finally:
+        loop.close()
+
+
+@app.route('/api/chat', methods=['POST'])
+def chat():
+    """
+    聊天端点 - 接收用户消息,返回 Claude 响应(支持 MCP 工具调用)
+    """
+    try:
+        data = request.json
+        message = data.get('message', '')
+        conversation_history = data.get('history', [])
+        session_id = request.headers.get('X-Session-ID')
+
+        if not message:
+            return jsonify({"error": "Message is required"}), 400
+
+        # 创建对话管理器
+        conv_manager = ConversationManager(
+            api_key=ANTHROPIC_API_KEY,
+            base_url=ANTHROPIC_BASE_URL,
+            model=ANTHROPIC_MODEL,
+            session_id=session_id
+        )
+
+        # 格式化对话历史
+        formatted_history = ConversationManager.format_history_for_claude(conversation_history)
+
+        # 执行多轮对话(自动处理工具调用)
+        result = run_async(conv_manager.chat(
+            user_message=message,
+            conversation_history=formatted_history,
+            max_turns=5
+        ))
+
+        # 提取响应文本
+        response_text = result.get("response", "")
+        tool_calls = result.get("tool_calls", [])
+
+        return jsonify({
+            "response": response_text,
+            "model": ANTHROPIC_MODEL,
+            "tool_calls": tool_calls,
+            "has_tools": len(tool_calls) > 0
+        })
+
+    except Exception as e:
+        import traceback
+        return jsonify({
+            "error": str(e),
+            "traceback": traceback.format_exc()
+        }), 500
+
+
+@app.route('/api/mcp/servers', methods=['GET'])
+def list_mcp_servers():
+    """获取已配置的 MCP 服务器列表"""
+    servers = []
+    for name, server in MCP_SERVERS.items():
+        servers.append({
+            "id": name,
+            "name": server.get("name", name),
+            "url": server.get("url", ""),
+            "auth_type": server.get("auth_type", "none"),
+            "enabled": server.get("enabled", False)
+        })
+    return jsonify({"servers": servers})
+
+
+@app.route('/api/mcp/tools', methods=['GET'])
+def list_mcp_tools():
+    """获取可用的 MCP 工具列表"""
+    try:
+        session_id = request.headers.get('X-Session-ID')
+
+        # 使用静态方法获取工具
+        tools = ConversationManager.get_tools(session_id=session_id)
+
+        return jsonify({
+            "tools": tools,
+            "count": len(tools)
+        })
+    except Exception as e:
+        import traceback
+        return jsonify({
+            "error": str(e),
+            "traceback": traceback.format_exc(),
+            "tools": []
+        }), 500
+
+
+# ========== 认证 API ==========
+
+@app.route('/api/auth/login', methods=['POST'])
+def login():
+    """
+    Novel Platform 用户登录
+    代理到实际的登录端点并返回 JWT Token
+    """
+    try:
+        data = request.json
+        username = data.get('username')
+        password = data.get('password')
+
+        if not username or not password:
+            return jsonify({"error": "Username and password are required"}), 400
+
+        # 查找需要 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 target_server:
+            return jsonify({"error": "No JWT-authenticated server configured"}), 400
+
+        # 构建登录 URL
+        base_url = target_server.get('base_url', '')
+        login_path = target_server.get('login_url', '/api/auth/login')
+        login_url = f"{base_url}{login_path}"
+
+        # 调用实际的登录接口(同步版本)
+        response = httpx.post(
+            login_url,
+            json={"username": username, "password": password},
+            timeout=30.0
+        )
+
+        if response.status_code == 200:
+            result = response.json()
+            import uuid
+            session_id = str(uuid.uuid4())
+
+            # 存储会话信息
+            auth_sessions[session_id] = {
+                "username": username,
+                "token": result.get("token"),
+                "refresh_token": result.get("refresh_token"),
+                "server": target_server.get("name")
+            }
+
+            return jsonify({
+                "success": True,
+                "session_id": session_id,
+                "username": username,
+                "server": target_server.get("name"),
+                "token": result.get("token")
+            })
+        else:
+            return jsonify({
+                "error": "Login failed",
+                "details": response.text
+            }), response.status_code
+
+    except Exception as e:
+        return jsonify({"error": str(e)}), 500
+
+
+@app.route('/api/auth/admin-login', methods=['POST'])
+def admin_login():
+    """
+    Novel Platform 管理员登录
+    """
+    try:
+        data = request.json
+        username = data.get('username')
+        password = data.get('password')
+
+        if not username or not password:
+            return jsonify({"error": "Username and password are required"}), 400
+
+        # 查找管理员 MCP 服务器
+        target_server = MCP_SERVERS.get('novel-platform-admin')
+        if not target_server:
+            return jsonify({"error": "Admin server not configured"}), 400
+
+        # 构建登录 URL
+        base_url = target_server.get('base_url', '')
+        login_path = target_server.get('login_url', '/api/auth/admin-login')
+        login_url = f"{base_url}{login_path}"
+
+        # 调用实际的登录接口
+        response = httpx.post(
+            login_url,
+            json={"username": username, "password": password},
+            timeout=30.0
+        )
+
+        if response.status_code == 200:
+            result = response.json()
+            import uuid
+            session_id = str(uuid.uuid4())
+
+            auth_sessions[session_id] = {
+                "username": username,
+                "token": result.get("token"),
+                "refresh_token": result.get("refresh_token"),
+                "server": target_server.get("name"),
+                "role": "admin"
+            }
+
+            return jsonify({
+                "success": True,
+                "session_id": session_id,
+                "username": username,
+                "server": target_server.get("name"),
+                "role": "admin",
+                "token": result.get("token")
+            })
+        else:
+            return jsonify({
+                "error": "Admin login failed",
+                "details": response.text
+            }), response.status_code
+
+    except Exception as e:
+        return jsonify({"error": str(e)}), 500
+
+
+@app.route('/api/auth/logout', methods=['POST'])
+def logout():
+    """登出并清除会话"""
+    try:
+        data = request.json
+        session_id = data.get('session_id')
+
+        if session_id and session_id in auth_sessions:
+            del auth_sessions[session_id]
+
+        return jsonify({"success": True})
+    except Exception as e:
+        return jsonify({"error": str(e)}), 500
+
+
+@app.route('/api/auth/status', methods=['GET'])
+def auth_status():
+    """检查认证状态"""
+    session_id = request.headers.get('X-Session-ID')
+
+    if session_id and session_id in auth_sessions:
+        session = auth_sessions[session_id]
+        return jsonify({
+            "authenticated": True,
+            "username": session.get("username"),
+            "server": session.get("server"),
+            "role": session.get("role", "user")
+        })
+
+    return jsonify({
+        "authenticated": False
+    })
+
+
+if __name__ == '__main__':
+    port = int(os.getenv('PORT', 5000))
+    debug = os.getenv('DEBUG', 'False').lower() == 'true'
+    app.run(host='0.0.0.0', port=port, debug=debug)

+ 54 - 0
backend/config.py

@@ -0,0 +1,54 @@
+"""
+MCP 服务器配置
+"""
+import os
+
+# MCP 服务器列表配置 - HTTP MCP 服务器
+MCP_SERVERS = {
+    "novel-translator": {
+        "name": "Novel Translator MCP",
+        "url": "https://d8d-ai-vscode-8080-223-236-template-6-group.dev.d8d.fun/mcp",
+        "auth_type": "none",
+        "enabled": True
+    },
+    "novel-platform-user": {
+        "name": "Novel Platform User MCP",
+        "url": "https://d8d-ai-vscode-238-xxxx.dev.d8d.fun/mcp",
+        "auth_type": "jwt",
+        "login_url": "/api/auth/login",
+        "base_url": "https://d8d-ai-vscode-8081-223-238-template-6-group.dev.d8d.fun",
+        "enabled": True
+    },
+    "novel-platform-admin": {
+        "name": "Novel Platform Admin MCP",
+        "url": "https://d8d-ai-vscode-238-xxxx.dev.d8d.fun/admin-mcp",
+        "auth_type": "jwt",
+        "login_url": "/api/auth/admin-login",
+        "base_url": "https://d8d-ai-vscode-8081-223-238-template-6-group.dev.d8d.fun",
+        "enabled": True
+    },
+}
+
+# NPM MCP 服务器 (本地进程)
+NPM_MCP_SERVERS = {
+    "playwright": {
+        "command": "npx",
+        "args": ["-y", "@executeautomation/mcp-server-playwright"],
+        "enabled": False
+    },
+    "web-reader": {
+        "command": "npx",
+        "args": ["-y", "web-reader-mcp"],
+        "enabled": False
+    },
+    "zai-mcp": {
+        "command": "npx",
+        "args": ["-y", "zai-mcp-server"],
+        "enabled": False
+    }
+}
+
+# Claude API 配置
+ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_AUTH_TOKEN", "")
+ANTHROPIC_BASE_URL = os.getenv("ANTHROPIC_BASE_URL", "https://api.anthropic.com")
+ANTHROPIC_MODEL = os.getenv("ANTHROPIC_MODEL", "claude-sonnet-4-20250514")

+ 212 - 0
backend/conversation_manager.py

@@ -0,0 +1,212 @@
+"""
+多轮对话管理器 - 处理包含工具调用的多轮对话
+"""
+import asyncio
+from typing import Dict, List, Any, Optional
+from anthropic import Anthropic
+from mcp_client import MCPClient
+from tool_converter import ToolConverter
+from tool_handler import ToolCallHandler
+
+
+class ConversationManager:
+    """管理包含工具调用的多轮对话"""
+
+    def __init__(
+        self,
+        api_key: str,
+        base_url: str,
+        model: str,
+        session_id: str = None
+    ):
+        self.api_key = api_key
+        self.base_url = base_url
+        self.model = model
+        self.session_id = session_id
+        self.tool_handler = ToolCallHandler(session_id=session_id)
+        self._cached_tools = None
+        self.client = Anthropic(api_key=api_key, base_url=base_url)
+
+    async def get_available_tools(self) -> List[Dict[str, Any]]:
+        """获取可用的 Claude 格式工具列表(带缓存)"""
+        if self._cached_tools is not None:
+            return self._cached_tools
+
+        # 从 MCP 服务器发现工具
+        mcp_tools = await MCPClient.get_all_tools_async(self.session_id)
+
+        # 转换为 Claude 格式
+        claude_tools = ToolConverter.convert_mcp_tools(mcp_tools)
+        self._cached_tools = claude_tools
+
+        return claude_tools
+
+    @classmethod
+    async def get_tools_async(cls, session_id: str = None) -> List[Dict[str, Any]]:
+        """
+        类方法:获取可用的工具列表(异步)
+
+        用于 API 端点直接调用,无需创建完整实例
+        """
+        mcp_tools = await MCPClient.get_all_tools_async(session_id)
+        return ToolConverter.convert_mcp_tools(mcp_tools)
+
+    @staticmethod
+    def get_tools(session_id: str = None) -> List[Dict[str, Any]]:
+        """
+        静态方法:获取可用的工具列表(同步)
+
+        用于 API 端点直接调用
+        """
+        return asyncio.run(ConversationManager.get_tools_async(session_id))
+
+    async def chat(
+        self,
+        user_message: str,
+        conversation_history: List[Dict[str, Any]] = None,
+        max_turns: int = 5
+    ) -> Dict[str, Any]:
+        """
+        执行多轮对话(自动处理工具调用)
+
+        Args:
+            user_message: 用户消息
+            conversation_history: 对话历史
+            max_turns: 最大对话轮数(防止无限循环)
+
+        Returns:
+            最终响应和对话历史
+        """
+        if conversation_history is None:
+            conversation_history = []
+
+        messages = conversation_history.copy()
+        messages.append({
+            "role": "user",
+            "content": user_message
+        })
+
+        current_messages = messages
+        response_text = ""
+        tool_calls_made = []
+
+        for turn in range(max_turns):
+            # 获取可用工具
+            tools = await self.get_available_tools()
+
+            # 调用 Claude API
+            if tools:
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=4096,
+                    messages=current_messages,
+                    tools=tools
+                )
+            else:
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=4096,
+                    messages=current_messages
+                )
+
+            # 检查响应中是否有 tool_use
+            content_blocks = []
+            tool_use_blocks = []
+            text_blocks = []
+
+            for block in response.content:
+                block_type = getattr(block, "type", None)
+
+                if block_type == "tool_use":
+                    # 工具调用块
+                    block_dict = {
+                        "type": "tool_use",
+                        "id": getattr(block, "id", ""),
+                        "name": getattr(block, "name", ""),
+                        "input": getattr(block, "input", {})
+                    }
+                    content_blocks.append(block_dict)
+                    tool_use_blocks.append(block_dict)
+                else:
+                    # 文本块
+                    text_content = getattr(block, "text", "")
+                    if text_content:
+                        text_blocks.append({
+                            "type": "text",
+                            "text": text_content
+                        })
+                        content_blocks.append({
+                            "type": "text",
+                            "text": text_content
+                        })
+                        response_text += text_content
+
+            # 如果没有工具调用,返回结果
+            if not tool_use_blocks:
+                return {
+                    "response": response_text,
+                    "messages": current_messages,
+                    "tool_calls": tool_calls_made
+                }
+
+            # 处理工具调用
+            tool_results = await self.tool_handler.process_tool_use_blocks(
+                tool_use_blocks
+            )
+
+            # 记录工具调用
+            for tr in tool_results:
+                tool_calls_made.append({
+                    "tool": tr.get("tool_name"),
+                    "result": tr.get("result", {})
+                })
+
+            # 构建工具结果消息
+            tool_result_message = ToolCallHandler.create_tool_result_message(
+                tool_results
+            )
+
+            # 添加到消息历史
+            current_messages.append({
+                "role": "assistant",
+                "content": content_blocks
+            })
+            current_messages.append(tool_result_message)
+
+        # 达到最大轮数
+        return {
+            "response": response_text,
+            "messages": current_messages,
+            "tool_calls": tool_calls_made,
+            "warning": "达到最大对话轮数"
+        }
+
+    @staticmethod
+    def format_history_for_claude(history: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+        """
+        格式化对话历史为 Claude API 格式
+
+        Args:
+            history: 原始对话历史
+
+        Returns:
+            Claude API 格式的消息列表
+        """
+        formatted = []
+
+        for msg in history:
+            role = msg.get("role")
+            content = msg.get("content")
+
+            if role == "user":
+                if isinstance(content, str):
+                    formatted.append({"role": "user", "content": content})
+                elif isinstance(content, list):
+                    formatted.append({"role": "user", "content": content})
+            elif role == "assistant":
+                if isinstance(content, str):
+                    formatted.append({"role": "assistant", "content": content})
+                elif isinstance(content, list):
+                    formatted.append({"role": "assistant", "content": content})
+
+        return formatted

+ 162 - 0
backend/mcp_client.py

@@ -0,0 +1,162 @@
+"""
+MCP 客户端 - 工具发现和调用(支持 SSE 响应)
+"""
+import json
+import httpx
+import asyncio
+import re
+from typing import Dict, List, Any, Optional
+from config import MCP_SERVERS
+
+
+def parse_sse_response(text: str) -> str:
+    """
+    解析 SSE 响应,提取 JSON 数据
+
+    SSE 格式:
+    event: message
+    data: {...json...}
+
+    Args:
+        text: SSE 响应文本
+
+    Returns:
+        提取的 JSON 字符串
+    """
+    # 查找 data: 行并提取 JSON
+    for line in text.split('\n'):
+        if line.startswith('data:'):
+            data_content = line[5:].strip()
+            if data_content:
+                return data_content
+    return text
+
+
+class MCPClient:
+    """MCP 客户端,负责工具发现和调用"""
+
+    def __init__(self, server_id: str = None, session_id: str = None):
+        self.server_id = server_id or "novel-translator"
+        self.server_config = MCP_SERVERS.get(self.server_id, {})
+        self.session_id = session_id
+        self.base_url = self.server_config.get("url", "")
+
+    async def discover_tools(self) -> List[Dict[str, Any]]:
+        """从 MCP 服务器发现可用工具"""
+        if not self.base_url:
+            return []
+
+        try:
+            async with httpx.AsyncClient(timeout=30.0) as client:
+                url = f"{self.base_url}"
+                headers = {
+                    "Content-Type": "application/json",
+                    "Accept": "application/json, text/event-stream"
+                }
+
+                if self.session_id:
+                    headers["X-Session-ID"] = self.session_id
+
+                payload = {
+                    "jsonrpc": "2.0",
+                    "id": 1,
+                    "method": "tools/list"
+                }
+
+                response = await client.post(url, json=payload, headers=headers)
+
+                if response.status_code == 200:
+                    # 解析 SSE 响应
+                    json_text = parse_sse_response(response.text)
+                    result = json.loads(json_text)
+
+                    if "result" in result:
+                        return result["result"].get("tools", [])
+
+                return []
+
+        except Exception as e:
+            print(f"MCP 工具发现失败: {e}")
+            import traceback
+            traceback.print_exc()
+            return []
+
+    async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
+        """调用 MCP 工具"""
+        if not self.base_url:
+            return {"error": "MCP 服务器未配置"}
+
+        try:
+            async with httpx.AsyncClient(timeout=120.0) as client:
+                url = f"{self.base_url}"
+                headers = {
+                    "Content-Type": "application/json",
+                    "Accept": "application/json, text/event-stream"
+                }
+
+                if self.session_id:
+                    headers["X-Session-ID"] = self.session_id
+
+                payload = {
+                    "jsonrpc": "2.0",
+                    "id": 2,
+                    "method": "tools/call",
+                    "params": {
+                        "name": tool_name,
+                        "arguments": arguments
+                    }
+                }
+
+                response = await client.post(url, json=payload, headers=headers)
+
+                if response.status_code == 200:
+                    # 解析 SSE 响应
+                    json_text = parse_sse_response(response.text)
+                    result = json.loads(json_text)
+
+                    if "result" in result:
+                        content_list = result["result"].get("content", [])
+                        text_results = []
+                        for item in content_list:
+                            if item.get("type") == "text":
+                                text_results.append(item.get("text", ""))
+                        return {
+                            "success": True,
+                            "result": "\n".join(text_results),
+                            "raw": result["result"]
+                        }
+                    elif "error" in result:
+                        return {"error": result["error"].get("message", "Unknown error")}
+
+                return {"error": f"工具调用失败: {response.status_code}"}
+
+        except Exception as e:
+            print(f"MCP 工具调用失败: {e}")
+            import traceback
+            traceback.print_exc()
+            return {"error": str(e)}
+
+    @staticmethod
+    async def get_all_tools_async(session_id: str = None) -> List[Dict[str, Any]]:
+        """获取所有已配置 MCP 服务器的工具列表(异步版本)"""
+        all_tools = []
+
+        for server_id in MCP_SERVERS.keys():
+            if not MCP_SERVERS[server_id].get("enabled", False):
+                continue
+
+            client = MCPClient(server_id, session_id)
+            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
+    def get_all_tools(session_id: str = None) -> List[Dict[str, Any]]:
+        """获取所有已配置 MCP 服务器的工具列表(同步版本)"""
+        return asyncio.run(MCPClient.get_all_tools_async(session_id))

+ 6 - 0
backend/requirements.txt

@@ -0,0 +1,6 @@
+flask==3.0.0
+flask-cors==4.0.0
+anthropic==0.40.0
+python-dotenv==1.0.0
+mcp==0.9.1
+httpx==0.27.0

+ 93 - 0
backend/simple_server.py

@@ -0,0 +1,93 @@
+"""
+AI MCP Web UI - 简化后端服务器
+使用 Python 内置 http.server 模块
+"""
+import os
+import json
+import http.server
+import socketserver
+from pathlib import Path
+
+# 配置
+PORT = int(os.getenv('PORT', 5000))
+FRONTEND_DIR = str(Path(__file__).parent.parent / 'frontend')
+
+class MCPHandler(http.server.SimpleHTTPRequestHandler):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, directory=FRONTEND_DIR, **kwargs)
+
+    def log_message(self, format, *args):
+        print(f"[{self.log_date_time_string()}] {format % args}")
+
+    def end_headers(self):
+        self.send_header('Access-Control-Allow-Origin', '*')
+        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
+        self.send_header('Access-Control-Allow-Headers', 'Content-Type, X-Session-ID')
+        super().end_headers()
+
+    def do_OPTIONS(self):
+        self.send_response(200)
+        self.end_headers()
+
+    def do_GET(self):
+        if self.path == '/api/health':
+            self.send_json_response(200, {
+                "status": "ok",
+                "message": "MCP Web UI Server (Simplified)",
+                "frontend_dir": FRONTEND_DIR
+            })
+        elif self.path == '/api/mcp/servers':
+            self.send_json_response(200, {
+                "servers": [
+                    {"id": "novel-translator", "name": "Novel Translator MCP", "enabled": True},
+                    {"id": "novel-platform-user", "name": "Novel Platform User MCP", "enabled": False},
+                    {"id": "novel-platform-admin", "name": "Novel Platform Admin MCP", "enabled": False}
+                ]
+            })
+        else:
+            super().do_GET()
+
+    def do_POST(self):
+        content_length = int(self.headers.get('Content-Length', 0))
+        data = {}
+        if content_length > 0:
+            try:
+                post_data = self.rfile.read(content_length)
+                data = json.loads(post_data.decode('utf-8'))
+            except:
+                pass
+
+        if self.path == '/api/chat':
+            message = data.get('message', '')
+            self.send_json_response(200, {
+                "response": f"[简化服务器] 收到: {message}\n\n注意: 完整 AI 功能需要安装 Flask、Anthropic SDK 等依赖。",
+                "model": "simplified-server"
+            })
+        elif self.path == '/api/auth/login':
+            self.send_json_response(200, {
+                "success": False,
+                "message": "完整认证功能需要 Flask 支持"
+            })
+        else:
+            self.send_json_response(404, {"error": "Not found"})
+
+    def send_json_response(self, code, data):
+        self.send_response(code)
+        self.send_header('Content-Type', 'application/json')
+        self.end_headers()
+        self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
+
+def main():
+    print("=" * 50)
+    print("  MCP Web UI - 简化服务器")
+    print("=" * 50)
+    print(f"  端口: {PORT}")
+    print(f"  前端: {FRONTEND_DIR}")
+    print(f"  访问: http://localhost:{PORT}")
+    print("=" * 50)
+
+    with socketserver.TCPServer(("0.0.0.0", PORT), MCPHandler) as httpd:
+        httpd.serve_forever()
+
+if __name__ == '__main__':
+    main()

+ 76 - 0
backend/tool_converter.py

@@ -0,0 +1,76 @@
+"""
+工具转换器 - 将 MCP 工具定义转换为 Claude API 工具格式
+"""
+from typing import Dict, List, Any
+import json
+
+
+class ToolConverter:
+    """将 MCP 工具转换为 Claude API 工具格式"""
+
+    @staticmethod
+    def mcp_to_claude_tool(mcp_tool: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        将单个 MCP 工具转换为 Claude API 工具格式
+
+        Args:
+            mcp_tool: MCP 工具定义,包含 name, description, inputSchema
+
+        Returns:
+            Claude API 工具格式
+        """
+        name = mcp_tool.get("name", "")
+        description = mcp_tool.get("description", "")
+        input_schema = mcp_tool.get("inputSchema", {})
+
+        # 构建 Claude 工具格式
+        claude_tool = {
+            "name": name,
+            "description": description,
+            "input_schema": {
+                "type": "object",
+                "properties": {},
+                "required": []
+            }
+        }
+
+        # 转换 inputSchema
+        if isinstance(input_schema, dict):
+            properties = input_schema.get("properties", {})
+            required = input_schema.get("required", [])
+
+            claude_tool["input_schema"]["properties"] = properties
+            claude_tool["input_schema"]["required"] = required
+
+            # 添加类型信息
+            for prop_name, prop_def in properties.items():
+                if isinstance(prop_def, dict) and "type" not in prop_def:
+                    # 尝试从 schema 中推断类型
+                    if "$ref" in prop_def:
+                        prop_def["type"] = "string"
+                    elif "enum" in prop_def:
+                        prop_def["type"] = "string"
+
+        return claude_tool
+
+    @staticmethod
+    def convert_mcp_tools(mcp_tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+        """
+        批量转换 MCP 工具列表
+
+        Args:
+            mcp_tools: MCP 工具列表
+
+        Returns:
+            Claude API 工具列表
+        """
+        claude_tools = []
+
+        for mcp_tool in mcp_tools:
+            try:
+                claude_tool = ToolConverter.mcp_to_claude_tool(mcp_tool)
+                claude_tools.append(claude_tool)
+            except Exception as e:
+                print(f"转换工具 {mcp_tool.get('name', 'unknown')} 失败: {e}")
+
+        return claude_tools

+ 135 - 0
backend/tool_handler.py

@@ -0,0 +1,135 @@
+"""
+工具调用处理器 - 处理 Claude 的 tool_use 响应
+"""
+import asyncio
+import json
+from typing import Dict, List, Any
+from mcp_client import MCPClient
+from tool_converter import ToolConverter
+
+
+class ToolCallHandler:
+    """处理 Claude 返回的 tool_use 类型内容块"""
+
+    def __init__(self, session_id: str = None):
+        self.session_id = session_id
+
+    async def process_tool_use_block(
+        self,
+        tool_use_block: Dict[str, Any]
+    ) -> Dict[str, Any]:
+        """
+        处理单个 tool_use 内容块
+
+        Args:
+            tool_use_block: Claude 返回的 tool_use 内容块
+                {
+                    "type": "tool_use",
+                    "id": "...",
+                    "name": "tool_name",
+                    "input": {...}
+                }
+
+        Returns:
+            工具执行结果
+        """
+        tool_name = tool_use_block.get("name", "")
+        tool_input = tool_use_block.get("input", {})
+        tool_id = tool_use_block.get("id", "")
+
+        if not tool_name:
+            return {
+                "tool_use_id": tool_id,
+                "error": "工具名称为空"
+            }
+
+        # 调用 MCP 工具
+        client = MCPClient(session_id=self.session_id)
+        result = await client.call_tool(tool_name, tool_input)
+
+        return {
+            "tool_use_id": tool_id,
+            "tool_name": tool_name,
+            "result": result
+        }
+
+    async def process_tool_use_blocks(
+        self,
+        content_blocks: List[Dict[str, Any]]
+    ) -> List[Dict[str, Any]]:
+        """
+        批量处理 tool_use 内容块
+
+        Args:
+            content_blocks: Claude 返回的内容块列表
+
+        Returns:
+            工具执行结果列表
+        """
+        tool_use_blocks = [
+            block for block in content_blocks
+            if block.get("type") == "tool_use"
+        ]
+
+        if not tool_use_blocks:
+            return []
+
+        # 并发执行所有工具调用
+        tasks = [
+            self.process_tool_use_block(block)
+            for block in tool_use_blocks
+        ]
+
+        results = await asyncio.gather(*tasks, return_exceptions=True)
+
+        # 处理异常
+        formatted_results = []
+        for i, result in enumerate(results):
+            if isinstance(result, Exception):
+                formatted_results.append({
+                    "tool_use_id": tool_use_blocks[i].get("id", ""),
+                    "error": str(result)
+                })
+            else:
+                formatted_results.append(result)
+
+        return formatted_results
+
+    @staticmethod
+    def create_tool_result_message(tool_results: List[Dict[str, Any]]) -> Dict[str, Any]:
+        """
+        将工具执行结果转换为 Claude API 可用的消息格式
+
+        Args:
+            tool_results: 工具执行结果列表
+
+        Returns:
+            Claude API 消息格式
+        """
+        content = []
+
+        for result in tool_results:
+            tool_use_id = result.get("tool_use_id", "")
+            tool_result = result.get("result", {})
+
+            # 检查是否有错误
+            if "error" in tool_result:
+                content.append({
+                    "type": "tool_result",
+                    "tool_use_id": tool_use_id,
+                    "content": f"错误: {tool_result['error']}",
+                    "is_error": True
+                })
+            else:
+                # 成功结果
+                result_text = tool_result.get("result", "")
+                content.append({
+                    "type": "tool_result",
+                    "tool_use_id": tool_use_id,
+                    "content": result_text
+                })
+
+        return {
+            "role": "user",
+            "content": content
+        }

+ 281 - 0
frontend/index.html

@@ -0,0 +1,281 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>AI MCP Web UI</title>
+    <script src="https://cdn.tailwindcss.com"></script>
+    <style>
+        .chat-container {
+            height: calc(100vh - 200px);
+        }
+        .message-user {
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+        }
+        .message-assistant {
+            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+        }
+        .typing-indicator span {
+            animation: bounce 1.4s infinite ease-in-out both;
+        }
+        .typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
+        .typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
+        @keyframes bounce {
+            0%, 80%, 100% { transform: scale(0); }
+            40% { transform: scale(1); }
+        }
+        pre {
+            background: #1e1e1e;
+            color: #d4d4d4;
+            padding: 1rem;
+            border-radius: 0.5rem;
+            overflow-x: auto;
+        }
+        code {
+            font-family: 'Courier New', monospace;
+        }
+    </style>
+</head>
+<body class="bg-gray-100 min-h-screen">
+    <div class="container mx-auto px-4 py-6 max-w-4xl">
+        <!-- Header -->
+        <header class="bg-white rounded-lg shadow-md p-4 mb-4">
+            <div class="flex items-center justify-between">
+                <div class="flex items-center space-x-3">
+                    <div class="w-10 h-10 bg-gradient-to-br from-purple-500 to-indigo-600 rounded-lg flex items-center justify-center">
+                        <span class="text-white font-bold">AI</span>
+                    </div>
+                    <div>
+                        <h1 class="text-xl font-bold text-gray-800">AI MCP Web UI</h1>
+                        <p class="text-sm text-gray-500">通用 MCP 服务器 Web 界面</p>
+                    </div>
+                </div>
+                <div id="status-indicator" class="flex items-center space-x-2">
+                    <span class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
+                    <span class="text-sm text-gray-600">已连接</span>
+                </div>
+            </div>
+        </header>
+
+        <!-- Chat Container -->
+        <div class="bg-white rounded-lg shadow-md">
+            <!-- Messages -->
+            <div id="chat-messages" class="chat-container overflow-y-auto p-4 space-y-4">
+                <!-- Welcome Message -->
+                <div class="flex justify-start">
+                    <div class="message-assistant rounded-lg p-4 max-w-[80%] shadow-sm">
+                        <p class="text-gray-800">你好!我是 AI MCP Web UI 助手。我可以帮助你:</p>
+                        <ul class="mt-2 text-gray-700 list-disc list-inside">
+                            <li>与 Claude AI 进行对话</li>
+                            <li>调用 MCP 服务器工具</li>
+                            <li>自动化各种任务</li>
+                        </ul>
+                        <p class="mt-2 text-gray-600 text-sm">请问有什么可以帮助你的?</p>
+                    </div>
+                </div>
+            </div>
+
+            <!-- Input Area -->
+            <div class="border-t border-gray-200 p-4">
+                <form id="chat-form" class="flex space-x-3">
+                    <input
+                        type="text"
+                        id="user-input"
+                        placeholder="输入你的消息..."
+                        class="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
+                        autocomplete="off"
+                    >
+                    <button
+                        type="submit"
+                        id="send-button"
+                        class="px-6 py-3 bg-gradient-to-r from-purple-500 to-indigo-600 text-white rounded-lg hover:from-purple-600 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
+                    >
+                        发送
+                    </button>
+                </form>
+            </div>
+        </div>
+
+        <!-- MCP Servers Status -->
+        <div class="mt-4 bg-white rounded-lg shadow-md p-4">
+            <h2 class="text-lg font-semibold text-gray-800 mb-3">MCP 服务器状态</h2>
+            <div id="mcp-servers" class="grid grid-cols-1 sm:grid-cols-3 gap-3">
+                <!-- MCP server cards will be inserted here -->
+            </div>
+        </div>
+    </div>
+
+    <script>
+        // State
+        let conversationHistory = [];
+        let isTyping = false;
+
+        // DOM Elements
+        const chatMessages = document.getElementById('chat-messages');
+        const chatForm = document.getElementById('chat-form');
+        const userInput = document.getElementById('user-input');
+        const sendButton = document.getElementById('send-button');
+        const mcpServers = document.getElementById('mcp-servers');
+        const statusIndicator = document.getElementById('status-indicator');
+
+        // API Base URL
+        const API_BASE = window.location.origin;
+
+        // Format message content (handle code blocks, etc.)
+        function formatMessage(content) {
+            // Simple markdown-like formatting
+            let formatted = content
+                .replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
+                .replace(/`([^`]+)`/g, '<code class="bg-gray-200 px-1 rounded">$1</code>')
+                .replace(/\n/g, '<br>');
+            return formatted;
+        }
+
+        // Add message to chat
+        function addMessage(role, content) {
+            const messageDiv = document.createElement('div');
+            messageDiv.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'}`;
+
+            const bubbleClass = role === 'user' ? 'message-user text-white' : 'message-assistant text-gray-800';
+            messageDiv.innerHTML = `
+                <div class="${bubbleClass} rounded-lg p-4 max-w-[80%] shadow-sm">
+                    <div>${formatMessage(content)}</div>
+                </div>
+            `;
+
+            chatMessages.appendChild(messageDiv);
+            chatMessages.scrollTop = chatMessages.scrollHeight;
+        }
+
+        // Add typing indicator
+        function addTypingIndicator() {
+            const typingDiv = document.createElement('div');
+            typingDiv.id = 'typing-indicator';
+            typingDiv.className = 'flex justify-start';
+            typingDiv.innerHTML = `
+                <div class="message-assistant rounded-lg p-4 shadow-sm">
+                    <div class="typing-indicator flex space-x-1">
+                        <span class="w-2 h-2 bg-gray-500 rounded-full"></span>
+                        <span class="w-2 h-2 bg-gray-500 rounded-full"></span>
+                        <span class="w-2 h-2 bg-gray-500 rounded-full"></span>
+                    </div>
+                </div>
+            `;
+            chatMessages.appendChild(typingDiv);
+            chatMessages.scrollTop = chatMessages.scrollHeight;
+        }
+
+        // Remove typing indicator
+        function removeTypingIndicator() {
+            const indicator = document.getElementById('typing-indicator');
+            if (indicator) indicator.remove();
+        }
+
+        // Send message to backend
+        async function sendMessage(message) {
+            if (isTyping) return;
+
+            isTyping = true;
+            sendButton.disabled = true;
+
+            // Add user message
+            addMessage('user', message);
+            conversationHistory.push({ role: 'user', content: message });
+
+            // Show typing indicator
+            addTypingIndicator();
+
+            try {
+                const response = await fetch(`${API_BASE}/api/chat`, {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json',
+                    },
+                    body: JSON.stringify({
+                        message: message,
+                        history: conversationHistory
+                    })
+                });
+
+                if (!response.ok) {
+                    throw new Error(`HTTP error! status: ${response.status}`);
+                }
+
+                const data = await response.json();
+
+                // Remove typing indicator
+                removeTypingIndicator();
+
+                // Add assistant message
+                addMessage('assistant', data.response);
+                conversationHistory.push({ role: 'assistant', content: data.response });
+
+            } catch (error) {
+                removeTypingIndicator();
+                addMessage('assistant', `错误: ${error.message}`);
+                console.error('Error:', error);
+            } finally {
+                isTyping = false;
+                sendButton.disabled = false;
+                userInput.focus();
+            }
+        }
+
+        // Load MCP servers
+        async function loadMCPServers() {
+            try {
+                const response = await fetch(`${API_BASE}/api/mcp/servers`);
+                const data = await response.json();
+
+                mcpServers.innerHTML = data.servers.map(server => `
+                    <div class="flex items-center space-x-2 p-3 rounded-lg ${server.enabled ? 'bg-green-50' : 'bg-gray-50'}">
+                        <span class="w-2 h-2 ${server.enabled ? 'bg-green-500' : 'bg-gray-400'} rounded-full"></span>
+                        <div>
+                            <p class="font-medium text-gray-800">${server.name}</p>
+                            <p class="text-xs text-gray-500 truncate">${server.url}</p>
+                            <p class="text-xs text-purple-600">${server.auth_type === 'jwt' ? '🔒 JWT 认证' : '🔓 无需认证'}</p>
+                        </div>
+                    </div>
+                `).join('');
+            } catch (error) {
+                console.error('Failed to load MCP servers:', error);
+            }
+        }
+
+        // Check health
+        async function checkHealth() {
+            try {
+                const response = await fetch(`${API_BASE}/api/health`);
+                if (response.ok) {
+                    statusIndicator.innerHTML = `
+                        <span class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
+                        <span class="text-sm text-gray-600">已连接</span>
+                    `;
+                } else {
+                    throw new Error('Health check failed');
+                }
+            } catch (error) {
+                statusIndicator.innerHTML = `
+                    <span class="w-2 h-2 bg-red-500 rounded-full"></span>
+                    <span class="text-sm text-gray-600">连接失败</span>
+                `;
+            }
+        }
+
+        // Event listeners
+        chatForm.addEventListener('submit', (e) => {
+            e.preventDefault();
+            const message = userInput.value.trim();
+            if (message) {
+                sendMessage(message);
+                userInput.value = '';
+            }
+        });
+
+        // Initialize
+        loadMCPServers();
+        checkHealth();
+        userInput.focus();
+    </script>
+</body>
+</html>