mirror of https://github.com/Mai-with-u/MaiBot.git
feat: vendor MCPBridgePlugin v2.0.0
- Update built-in MCP bridge plugin to v2.0.0 (Claude mcpServers config)\n- Preserve Workflow (toolchains) + ReAct dual-track\n- Fix WebUI status display persistence and reduce workflow registration noise\n- Default plugin disabled in WebUI\n- Add CHANGELOG.md and refactor docs; remove test scriptspull/1451/head
parent
8dba63eb5c
commit
b2ac055921
|
|
@ -0,0 +1,30 @@
|
|||
# 运行时配置(包含用户敏感信息)
|
||||
config.toml
|
||||
|
||||
# 备份文件
|
||||
*.backup.*
|
||||
*.bak
|
||||
|
||||
# 日志
|
||||
logs/
|
||||
*.log
|
||||
*.jsonl
|
||||
|
||||
# Python 缓存
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
|
||||
# 本地测试脚本(仓库不提交)
|
||||
test_*.py
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Changelog
|
||||
|
||||
本文件记录 `MaiBot_MCPBridgePlugin` 的用户可感知变更。
|
||||
|
||||
## 2.0.0
|
||||
|
||||
- 配置入口统一:MCP 服务器仅使用 Claude Desktop `mcpServers` JSON(`servers.claude_config_json`)
|
||||
- 兼容迁移:自动识别旧版 `servers.list` 并迁移为 `mcpServers`(需在 WebUI 保存一次固化)
|
||||
- 保持功能不变:保留 Workflow(硬流程/工具链)与 ReAct(软流程)双轨制能力
|
||||
- 精简实现:移除旧的 WebUI 导入导出/快速添加服务器实现与 `tomlkit` 依赖
|
||||
- 易用性:完善 Workflow 变量替换(支持数组下标与 bracket 写法),并优化 WebUI 配置区顺序
|
||||
|
||||
## 1.9.0
|
||||
|
||||
- 双轨制架构:ReAct(软流程)+ Workflow(硬流程/工具链)
|
||||
|
||||
## 1.8.0
|
||||
|
||||
- Workflow(工具链):多工具顺序执行、变量替换、自定义 Workflow 并注册为组合工具
|
||||
|
||||
## 1.7.0
|
||||
|
||||
- 断路器模式、状态刷新、工具搜索等易用性增强
|
||||
|
||||
|
|
@ -1,569 +1,356 @@
|
|||
# MCP 桥接插件 - 开发文档
|
||||
# MCP 桥接插件开发文档
|
||||
|
||||
本文档面向 AI 助手或开发者进行插件开发/维护。
|
||||
本文档面向开发者,介绍插件的架构设计、核心模块和扩展方式。
|
||||
|
||||
## 前置知识
|
||||
|
||||
本插件基于 MaiBot 插件系统开发,需要了解:
|
||||
- MaiBot 插件框架:`BasePlugin`, `BaseTool`, `BaseCommand`, `BaseEventHandler`
|
||||
- 配置系统:`ConfigField`, `config_schema`
|
||||
- 组件注册:`component_registry.register_component()`
|
||||
|
||||
详见项目根目录 `.kiro/steering/plugin-dev.md`。
|
||||
|
||||
---
|
||||
|
||||
## 版本历史
|
||||
|
||||
| 版本 | 主要功能 |
|
||||
|------|----------|
|
||||
| v1.5.4 | 易用性优化:新增 MCP 服务器获取快捷入口 |
|
||||
| v1.5.3 | 配置优化:新增智能心跳 WebUI 配置项 |
|
||||
| v1.5.2 | 性能优化:智能心跳间隔,根据服务器稳定性动态调整 |
|
||||
| v1.5.1 | 易用性优化:新增「快速添加服务器」表单式配置 |
|
||||
| v1.5.0 | 性能优化:服务器并行连接,大幅减少启动时间 |
|
||||
| v1.4.4 | 修复首次生成默认配置文件时多行字符串导致 TOML 解析失败 |
|
||||
| v1.4.3 | 修复 WebUI 保存配置后多行字符串格式错误导致配置文件无法读取 |
|
||||
| v1.4.2 | HTTP 鉴权头支持(headers 字段) |
|
||||
| v1.4.0 | 工具禁用、调用追踪、缓存、权限控制、WebUI 易用性改进 |
|
||||
| v1.3.0 | 结果后处理(LLM 摘要提炼) |
|
||||
| v1.2.0 | Resources/Prompts 支持(实验性) |
|
||||
| v1.1.x | 心跳检测、自动重连、调用统计、`/mcp` 命令 |
|
||||
| v1.0.0 | 基础 MCP 桥接 |
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
MCPBridgePlugin/
|
||||
├── plugin.py # 主插件逻辑(1800+ 行)
|
||||
├── mcp_client.py # MCP 客户端封装(800+ 行)
|
||||
├── _manifest.json # 插件清单
|
||||
├── config.example.toml # 配置示例
|
||||
├── requirements.txt # 依赖:mcp>=1.0.0
|
||||
├── README.md # 用户文档
|
||||
└── DEVELOPMENT.md # 开发文档(本文件)
|
||||
MaiBot_MCPBridgePlugin/
|
||||
├── plugin.py # 主插件文件,包含所有核心逻辑
|
||||
├── mcp_client.py # MCP 客户端封装
|
||||
├── tool_chain.py # 工具链(Workflow)模块
|
||||
├── core/
|
||||
│ └── claude_config.py # Claude Desktop mcpServers 解析/迁移
|
||||
├── config.toml # 运行时配置
|
||||
└── _manifest.json # 插件元数据
|
||||
```
|
||||
|
||||
---
|
||||
## 核心模块
|
||||
|
||||
## 核心模块详解
|
||||
### 1. MCP 客户端 (`mcp_client.py`)
|
||||
|
||||
### 1. mcp_client.py - MCP 客户端
|
||||
|
||||
负责与 MCP 服务器通信,可独立于 MaiBot 运行测试。
|
||||
|
||||
#### 数据类
|
||||
封装了与 MCP 服务器的通信逻辑。
|
||||
|
||||
```python
|
||||
class TransportType(Enum):
|
||||
STDIO = "stdio" # 本地进程
|
||||
SSE = "sse" # Server-Sent Events
|
||||
HTTP = "http" # HTTP
|
||||
STREAMABLE_HTTP = "streamable_http" # HTTP Streamable(推荐)
|
||||
from .mcp_client import mcp_manager, MCPServerConfig, TransportType
|
||||
|
||||
@dataclass
|
||||
class MCPServerConfig:
|
||||
name: str # 服务器唯一标识
|
||||
enabled: bool = True
|
||||
transport: TransportType = TransportType.STDIO
|
||||
command: str = "" # stdio: 启动命令
|
||||
args: List[str] = field(default_factory=list) # stdio: 参数
|
||||
env: Dict[str, str] = field(default_factory=dict) # stdio: 环境变量
|
||||
url: str = "" # http/sse: 服务器 URL
|
||||
# 添加服务器
|
||||
config = MCPServerConfig(
|
||||
name="my-server",
|
||||
transport=TransportType.STREAMABLE_HTTP,
|
||||
url="https://mcp.example.com/mcp"
|
||||
)
|
||||
await mcp_manager.add_server(config)
|
||||
|
||||
@dataclass
|
||||
class MCPToolInfo:
|
||||
name: str # 工具原始名称
|
||||
description: str
|
||||
input_schema: Dict[str, Any] # JSON Schema
|
||||
server_name: str
|
||||
|
||||
@dataclass
|
||||
class MCPCallResult:
|
||||
success: bool
|
||||
content: str = ""
|
||||
error: Optional[str] = None
|
||||
duration_ms: float = 0.0
|
||||
|
||||
@dataclass
|
||||
class MCPResourceInfo:
|
||||
uri: str
|
||||
name: str
|
||||
description: str = ""
|
||||
mime_type: Optional[str] = None
|
||||
server_name: str = ""
|
||||
|
||||
@dataclass
|
||||
class MCPPromptInfo:
|
||||
name: str
|
||||
description: str = ""
|
||||
arguments: List[Dict[str, Any]] = field(default_factory=list)
|
||||
server_name: str = ""
|
||||
# 调用工具
|
||||
result = await mcp_manager.call_tool("server_tool_name", {"param": "value"})
|
||||
if result.success:
|
||||
print(result.content)
|
||||
```
|
||||
|
||||
#### MCPClientSession
|
||||
**支持的传输类型:**
|
||||
- `STDIO`: 本地进程通信
|
||||
- `SSE`: Server-Sent Events
|
||||
- `HTTP`: HTTP 请求
|
||||
- `STREAMABLE_HTTP`: 流式 HTTP(推荐)
|
||||
|
||||
管理单个 MCP 服务器连接。
|
||||
|
||||
```python
|
||||
class MCPClientSession:
|
||||
def __init__(self, config: MCPServerConfig): ...
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""连接服务器,返回是否成功"""
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
"""断开连接"""
|
||||
|
||||
async def call_tool(self, tool_name: str, arguments: Dict) -> MCPCallResult:
|
||||
"""调用工具"""
|
||||
|
||||
async def check_health(self) -> bool:
|
||||
"""健康检查(用于心跳)"""
|
||||
|
||||
async def fetch_resources(self) -> bool:
|
||||
"""获取资源列表"""
|
||||
|
||||
async def read_resource(self, uri: str) -> MCPCallResult:
|
||||
"""读取资源"""
|
||||
|
||||
async def fetch_prompts(self) -> bool:
|
||||
"""获取提示模板列表"""
|
||||
|
||||
async def get_prompt(self, name: str, arguments: Optional[Dict]) -> MCPCallResult:
|
||||
"""获取提示模板"""
|
||||
|
||||
@property
|
||||
def tools(self) -> List[MCPToolInfo]: ...
|
||||
@property
|
||||
def resources(self) -> List[MCPResourceInfo]: ...
|
||||
@property
|
||||
def prompts(self) -> List[MCPPromptInfo]: ...
|
||||
@property
|
||||
def is_connected(self) -> bool: ...
|
||||
```
|
||||
|
||||
#### MCPClientManager
|
||||
|
||||
全局单例,管理多服务器。
|
||||
|
||||
```python
|
||||
class MCPClientManager:
|
||||
def configure(self, settings: Dict) -> None:
|
||||
"""配置超时、重试等参数"""
|
||||
|
||||
async def add_server(self, config: MCPServerConfig) -> bool:
|
||||
"""添加并连接服务器"""
|
||||
|
||||
async def remove_server(self, server_name: str) -> bool:
|
||||
"""移除服务器"""
|
||||
|
||||
async def reconnect_server(self, server_name: str) -> bool:
|
||||
"""重连服务器"""
|
||||
|
||||
async def call_tool(self, tool_key: str, arguments: Dict) -> MCPCallResult:
|
||||
"""调用工具,tool_key 格式: mcp_{server}_{tool}"""
|
||||
|
||||
async def start_heartbeat(self) -> None:
|
||||
"""启动心跳检测"""
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""关闭所有连接"""
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""获取状态"""
|
||||
|
||||
def get_all_stats(self) -> Dict[str, Any]:
|
||||
"""获取统计信息"""
|
||||
|
||||
def set_status_change_callback(self, callback: Callable) -> None:
|
||||
"""设置状态变化回调"""
|
||||
|
||||
@property
|
||||
def all_tools(self) -> Dict[str, Tuple[MCPToolInfo, MCPClientSession]]: ...
|
||||
@property
|
||||
def all_resources(self) -> Dict[str, Tuple[MCPResourceInfo, MCPClientSession]]: ...
|
||||
@property
|
||||
def all_prompts(self) -> Dict[str, Tuple[MCPPromptInfo, MCPClientSession]]: ...
|
||||
@property
|
||||
def disconnected_servers(self) -> List[str]: ...
|
||||
|
||||
# 全局单例
|
||||
mcp_manager = MCPClientManager()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. plugin.py - MaiBot 插件
|
||||
|
||||
#### v1.4.0 新增模块
|
||||
|
||||
```python
|
||||
# ============ 调用追踪 ============
|
||||
@dataclass
|
||||
class ToolCallRecord:
|
||||
call_id: str # UUID
|
||||
timestamp: float
|
||||
tool_name: str
|
||||
server_name: str
|
||||
chat_id: str = ""
|
||||
user_id: str = ""
|
||||
user_query: str = ""
|
||||
arguments: Dict = field(default_factory=dict)
|
||||
raw_result: str = ""
|
||||
processed_result: str = ""
|
||||
duration_ms: float = 0.0
|
||||
success: bool = True
|
||||
error: str = ""
|
||||
post_processed: bool = False
|
||||
cache_hit: bool = False
|
||||
|
||||
class ToolCallTracer:
|
||||
def configure(self, enabled: bool, max_records: int, log_enabled: bool, log_path: Path): ...
|
||||
def record(self, record: ToolCallRecord) -> None: ...
|
||||
def get_recent(self, n: int = 10) -> List[ToolCallRecord]: ...
|
||||
def get_by_tool(self, tool_name: str) -> List[ToolCallRecord]: ...
|
||||
def clear(self) -> None: ...
|
||||
|
||||
tool_call_tracer = ToolCallTracer()
|
||||
|
||||
# ============ 调用缓存 ============
|
||||
@dataclass
|
||||
class CacheEntry:
|
||||
tool_name: str
|
||||
args_hash: str # MD5(tool_name + sorted_json_args)
|
||||
result: str
|
||||
created_at: float
|
||||
expires_at: float
|
||||
hit_count: int = 0
|
||||
|
||||
class ToolCallCache:
|
||||
def configure(self, enabled: bool, ttl: int, max_entries: int, exclude_tools: str): ...
|
||||
def get(self, tool_name: str, args: Dict) -> Optional[str]: ...
|
||||
def set(self, tool_name: str, args: Dict, result: str) -> None: ...
|
||||
def clear(self) -> None: ...
|
||||
def get_stats(self) -> Dict[str, Any]: ...
|
||||
|
||||
tool_call_cache = ToolCallCache()
|
||||
|
||||
# ============ 权限控制 ============
|
||||
class PermissionChecker:
|
||||
def configure(self, enabled: bool, default_mode: str, rules_json: str,
|
||||
quick_deny_groups: str = "", quick_allow_users: str = ""): ...
|
||||
def check(self, tool_name: str, chat_id: str, user_id: str, is_group: bool) -> bool: ...
|
||||
def get_rules_for_tool(self, tool_name: str) -> List[Dict]: ...
|
||||
|
||||
permission_checker = PermissionChecker()
|
||||
```
|
||||
|
||||
#### 工具代理
|
||||
### 2. 工具注册系统
|
||||
|
||||
MCP 工具通过动态类创建注册到 MaiBot:
|
||||
|
||||
```python
|
||||
# 创建工具代理类
|
||||
class MCPToolProxy(BaseTool):
|
||||
"""所有 MCP 工具的基类"""
|
||||
name = "mcp_server_tool"
|
||||
description = "工具描述"
|
||||
parameters = [("param", ToolParamType.STRING, "参数描述", True, None)]
|
||||
available_for_llm = True
|
||||
|
||||
# 类属性(动态子类覆盖)
|
||||
name: str = ""
|
||||
description: str = ""
|
||||
parameters: List[Tuple] = []
|
||||
available_for_llm: bool = True
|
||||
|
||||
# MCP 属性
|
||||
_mcp_tool_key: str = ""
|
||||
_mcp_original_name: str = ""
|
||||
_mcp_server_name: str = ""
|
||||
|
||||
async def execute(self, function_args: Dict) -> Dict[str, Any]:
|
||||
"""执行流程:
|
||||
1. 权限检查 → 拒绝则返回错误
|
||||
2. 缓存检查 → 命中则返回缓存
|
||||
3. 调用 MCP 服务器
|
||||
4. 存入缓存
|
||||
5. 后处理(可选)
|
||||
6. 记录追踪
|
||||
7. 返回结果
|
||||
"""
|
||||
|
||||
def create_mcp_tool_class(tool_key: str, tool_info: MCPToolInfo,
|
||||
tool_prefix: str, disabled: bool = False) -> Type[MCPToolProxy]:
|
||||
"""动态创建工具类"""
|
||||
async def execute(self, function_args):
|
||||
result = await mcp_manager.call_tool(self._mcp_tool_key, function_args)
|
||||
return {"name": self.name, "content": result.content}
|
||||
```
|
||||
|
||||
#### 内置工具
|
||||
### 3. 工具链模块 (`tool_chain.py`)
|
||||
|
||||
实现 Workflow 硬流程,支持多工具顺序执行。
|
||||
|
||||
```python
|
||||
class MCPStatusTool(BaseTool):
|
||||
"""mcp_status - 查询状态/工具/资源/模板/统计/追踪/缓存"""
|
||||
name = "mcp_status"
|
||||
parameters = [
|
||||
("query_type", STRING, "查询类型", False,
|
||||
["status", "tools", "resources", "prompts", "stats", "trace", "cache", "all"]),
|
||||
("server_name", STRING, "服务器名称", False, None),
|
||||
from .tool_chain import ToolChainDefinition, ToolChainStep, tool_chain_manager
|
||||
|
||||
# 定义工具链
|
||||
chain = ToolChainDefinition(
|
||||
name="search_and_detail",
|
||||
description="搜索并获取详情",
|
||||
input_params={"query": "搜索关键词"},
|
||||
steps=[
|
||||
ToolChainStep(
|
||||
tool_name="mcp_server_search",
|
||||
args_template={"keyword": "${input.query}"},
|
||||
output_key="search_result"
|
||||
),
|
||||
ToolChainStep(
|
||||
tool_name="mcp_server_detail",
|
||||
args_template={"id": "${prev}"}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
class MCPReadResourceTool(BaseTool):
|
||||
"""mcp_read_resource - 读取资源"""
|
||||
name = "mcp_read_resource"
|
||||
|
||||
class MCPGetPromptTool(BaseTool):
|
||||
"""mcp_get_prompt - 获取提示模板"""
|
||||
name = "mcp_get_prompt"
|
||||
# 注册并执行
|
||||
tool_chain_manager.add_chain(chain)
|
||||
result = await tool_chain_manager.execute_chain("search_and_detail", {"query": "test"})
|
||||
```
|
||||
|
||||
#### 命令
|
||||
**变量替换语法:**
|
||||
- `${input.参数名}`: 用户输入
|
||||
- `${step.输出键}`: 指定步骤的输出
|
||||
- `${prev}`: 上一步输出
|
||||
- `${prev.字段}`: 上一步输出(JSON)的字段
|
||||
- `${step.geo.return.0.location}` / `${step.geo.return[0].location}`: 数组下标访问
|
||||
- `${step.geo['return'][0]['location']}`: bracket 写法(最通用)
|
||||
|
||||
## 双轨制架构
|
||||
|
||||
### ReAct 软流程
|
||||
|
||||
将 MCP 工具注册到 MaiBot 的记忆检索 ReAct 系统,LLM 自主决策调用。
|
||||
|
||||
```python
|
||||
class MCPStatusCommand(BaseCommand):
|
||||
"""处理 /mcp 命令"""
|
||||
command_pattern = r"^[//]mcp(?:\s+(?P<subcommand>status|tools|stats|reconnect|trace|cache|perm))?(?:\s+(?P<arg>\S+))?$"
|
||||
def _register_tools_to_react(self) -> int:
|
||||
from src.memory_system.retrieval_tools import register_memory_retrieval_tool
|
||||
|
||||
# 子命令处理
|
||||
async def _handle_reconnect(self, server_name): ...
|
||||
async def _handle_trace(self, arg): ...
|
||||
async def _handle_cache(self, arg): ...
|
||||
async def _handle_perm(self, arg): ...
|
||||
def make_execute_func(tool_key: str):
|
||||
async def execute_func(**kwargs) -> str:
|
||||
result = await mcp_manager.call_tool(tool_key, kwargs)
|
||||
return result.content if result.success else f"失败: {result.error}"
|
||||
return execute_func
|
||||
|
||||
register_memory_retrieval_tool(
|
||||
name="mcp_tool_name",
|
||||
description="工具描述",
|
||||
parameters=[{"name": "param", "type": "string", "required": True}],
|
||||
execute_func=make_execute_func("tool_key")
|
||||
)
|
||||
```
|
||||
|
||||
#### 事件处理器
|
||||
### Workflow 硬流程
|
||||
|
||||
用户预定义的固定执行流程,注册为组合工具。
|
||||
|
||||
```python
|
||||
def _register_tool_chains(self) -> None:
|
||||
from src.plugin_system.core.component_registry import component_registry
|
||||
|
||||
for chain_name, chain in tool_chain_manager.get_enabled_chains().items():
|
||||
info, tool_class = tool_chain_registry.register_chain(chain)
|
||||
info.plugin_name = self.plugin_name
|
||||
component_registry.register_component(info, tool_class)
|
||||
```
|
||||
|
||||
## 配置系统
|
||||
|
||||
### MCP 服务器配置(Claude Desktop 规范)
|
||||
|
||||
插件只接受 Claude Desktop 的 `mcpServers` JSON(见 `core/claude_config.py`)。配置入口统一为:
|
||||
|
||||
- WebUI/配置文件:`[servers].claude_config_json`
|
||||
- 命令:`/mcp import`(合并 `mcpServers`)与 `/mcp export`(导出当前 `mcpServers`)
|
||||
|
||||
兼容迁移:
|
||||
- 若检测到旧版 `servers.list`,会自动迁移为 `servers.claude_config_json`(仅迁移到内存配置,需 WebUI 保存一次固化)。
|
||||
|
||||
### WebUI 配置 Schema
|
||||
|
||||
使用 `ConfigField` 定义 WebUI 配置项:
|
||||
|
||||
```python
|
||||
config_schema = {
|
||||
"section_name": {
|
||||
"field_name": ConfigField(
|
||||
type=str, # 类型: str, bool, int, float
|
||||
default="default_value", # 默认值
|
||||
description="字段描述",
|
||||
label="显示标签",
|
||||
input_type="textarea", # 输入类型: text, textarea, password
|
||||
rows=5, # textarea 行数
|
||||
disabled=True, # 只读
|
||||
choices=["a", "b"], # 下拉选项
|
||||
hint="提示信息",
|
||||
order=1, # 排序
|
||||
),
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 配置读取
|
||||
|
||||
```python
|
||||
# 在组件中读取配置
|
||||
value = self.get_config("section.key", default="fallback")
|
||||
|
||||
# 在插件类中读取
|
||||
value = self.config.get("section", {}).get("key", "default")
|
||||
```
|
||||
|
||||
## 事件处理
|
||||
|
||||
### 启动事件
|
||||
|
||||
```python
|
||||
class MCPStartupHandler(BaseEventHandler):
|
||||
"""ON_START - 连接服务器、注册工具"""
|
||||
event_type = EventType.ON_START
|
||||
handler_name = "mcp_startup"
|
||||
|
||||
async def execute(self, message):
|
||||
global _plugin_instance
|
||||
if _plugin_instance:
|
||||
await _plugin_instance._async_connect_servers()
|
||||
return (True, True, None, None, None)
|
||||
```
|
||||
|
||||
### 停止事件
|
||||
|
||||
```python
|
||||
class MCPStopHandler(BaseEventHandler):
|
||||
"""ON_STOP - 关闭连接"""
|
||||
event_type = EventType.ON_STOP
|
||||
handler_name = "mcp_stop"
|
||||
|
||||
async def execute(self, message):
|
||||
await mcp_manager.shutdown()
|
||||
return (True, True, None, None, None)
|
||||
```
|
||||
|
||||
#### 主插件类
|
||||
## 命令系统
|
||||
|
||||
```python
|
||||
@register_plugin
|
||||
class MCPBridgePlugin(BasePlugin):
|
||||
plugin_name = "mcp_bridge_plugin"
|
||||
python_dependencies = ["mcp"]
|
||||
class MCPStatusCommand(BaseCommand):
|
||||
command_name = "mcp_status"
|
||||
command_pattern = r"^/mcp(?:\s+(?P<action>\S+))?(?:\s+(?P<arg>.+))?$"
|
||||
|
||||
config_section_descriptions = {
|
||||
"guide": "📖 快速入门",
|
||||
"servers": "🔌 服务器配置",
|
||||
"status": "📊 运行状态",
|
||||
"plugin": "插件开关",
|
||||
"settings": "⚙️ 高级设置",
|
||||
"tools": "🔧 工具管理",
|
||||
"permissions": "🔐 权限控制",
|
||||
}
|
||||
|
||||
config_schema = {
|
||||
"guide": { "quick_start": ConfigField(...) },
|
||||
"plugin": { "enabled": ConfigField(...) },
|
||||
"settings": {
|
||||
# 基础:tool_prefix, connect_timeout, call_timeout, auto_connect, retry_*
|
||||
# 心跳:heartbeat_enabled, heartbeat_interval, auto_reconnect, max_reconnect_attempts
|
||||
# 高级:enable_resources, enable_prompts
|
||||
# 后处理:post_process_*
|
||||
# 追踪:trace_*
|
||||
# 缓存:cache_*
|
||||
},
|
||||
"tools": { "tool_list", "disabled_tools" },
|
||||
"permissions": { "perm_enabled", "perm_default_mode", "quick_deny_groups", "quick_allow_users", "perm_rules" },
|
||||
"servers": { "list" },
|
||||
"status": { "connection_status" },
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
# 配置 mcp_manager, tool_call_tracer, tool_call_cache, permission_checker
|
||||
|
||||
async def _async_connect_servers(self):
|
||||
# 解析配置 → 连接服务器 → 注册工具(检查禁用列表)
|
||||
|
||||
def _update_status_display(self):
|
||||
# 更新 WebUI 状态显示
|
||||
|
||||
def _update_tool_list_display(self):
|
||||
# 更新工具清单显示
|
||||
async def execute(self) -> Tuple[bool, str, bool]:
|
||||
action = self.matched_groups.get("action", "")
|
||||
arg = self.matched_groups.get("arg", "")
|
||||
|
||||
if action == "tools":
|
||||
await self.send_text("工具列表...")
|
||||
elif action == "reconnect":
|
||||
await self._handle_reconnect(arg)
|
||||
|
||||
return (True, None, True) # (成功, 消息, 拦截)
|
||||
```
|
||||
|
||||
---
|
||||
## 高级功能
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
MaiBot 启动
|
||||
│
|
||||
▼
|
||||
MCPBridgePlugin.__init__()
|
||||
├─ mcp_manager.configure(settings)
|
||||
├─ tool_call_tracer.configure(...)
|
||||
├─ tool_call_cache.configure(...)
|
||||
└─ permission_checker.configure(...)
|
||||
│
|
||||
▼
|
||||
ON_START 事件 → MCPStartupHandler.execute()
|
||||
│
|
||||
▼
|
||||
_async_connect_servers()
|
||||
├─ 解析 servers.list JSON
|
||||
├─ 遍历服务器配置
|
||||
│ ├─ mcp_manager.add_server(config)
|
||||
│ ├─ 获取工具列表
|
||||
│ ├─ 检查 disabled_tools
|
||||
│ └─ component_registry.register_component(tool_info, tool_class)
|
||||
├─ _update_status_display()
|
||||
└─ _update_tool_list_display()
|
||||
│
|
||||
▼
|
||||
mcp_manager.start_heartbeat()
|
||||
│
|
||||
▼
|
||||
LLM 调用工具 → MCPToolProxy.execute(function_args)
|
||||
├─ 1. permission_checker.check() → 拒绝则返回错误
|
||||
├─ 2. tool_call_cache.get() → 命中则跳到步骤 5
|
||||
├─ 3. mcp_manager.call_tool()
|
||||
├─ 4. tool_call_cache.set()
|
||||
├─ 5. _post_process_result() (如果启用且超过阈值)
|
||||
├─ 6. tool_call_tracer.record()
|
||||
└─ 7. 返回 {"name": ..., "content": ...}
|
||||
│
|
||||
▼
|
||||
ON_STOP 事件 → MCPStopHandler.execute()
|
||||
│
|
||||
▼
|
||||
mcp_manager.shutdown()
|
||||
mcp_tool_registry.clear()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置项速查
|
||||
|
||||
### settings(高级设置)
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| tool_prefix | str | "mcp" | 工具名前缀 |
|
||||
| connect_timeout | float | 30.0 | 连接超时(秒) |
|
||||
| call_timeout | float | 60.0 | 调用超时(秒) |
|
||||
| auto_connect | bool | true | 自动连接 |
|
||||
| retry_attempts | int | 3 | 重试次数 |
|
||||
| retry_interval | float | 5.0 | 重试间隔 |
|
||||
| heartbeat_enabled | bool | true | 心跳检测 |
|
||||
| heartbeat_interval | float | 60.0 | 心跳间隔 |
|
||||
| auto_reconnect | bool | true | 自动重连 |
|
||||
| max_reconnect_attempts | int | 3 | 最大重连次数 |
|
||||
| enable_resources | bool | false | Resources 支持 |
|
||||
| enable_prompts | bool | false | Prompts 支持 |
|
||||
| post_process_enabled | bool | false | 结果后处理 |
|
||||
| post_process_threshold | int | 500 | 后处理阈值 |
|
||||
| trace_enabled | bool | true | 调用追踪 |
|
||||
| trace_max_records | int | 100 | 追踪记录上限 |
|
||||
| cache_enabled | bool | false | 调用缓存 |
|
||||
| cache_ttl | int | 300 | 缓存 TTL |
|
||||
| cache_max_entries | int | 200 | 最大缓存条目 |
|
||||
|
||||
### permissions(权限控制)
|
||||
|
||||
| 配置项 | 说明 |
|
||||
|--------|------|
|
||||
| perm_enabled | 启用权限控制 |
|
||||
| perm_default_mode | allow_all / deny_all |
|
||||
| quick_deny_groups | 禁用群列表(每行一个群号) |
|
||||
| quick_allow_users | 管理员白名单(每行一个 QQ 号) |
|
||||
| perm_rules | 高级规则 JSON |
|
||||
|
||||
---
|
||||
|
||||
## 扩展开发示例
|
||||
|
||||
### 添加新命令子命令
|
||||
### 调用追踪
|
||||
|
||||
```python
|
||||
# 1. 修改 command_pattern
|
||||
command_pattern = r"^[//]mcp(?:\s+(?P<subcommand>status|...|newcmd))?..."
|
||||
from plugin import tool_call_tracer, ToolCallRecord
|
||||
|
||||
# 2. 在 execute() 添加分支
|
||||
if subcommand == "newcmd":
|
||||
return await self._handle_newcmd(arg)
|
||||
# 记录调用
|
||||
record = ToolCallRecord(
|
||||
call_id="xxx",
|
||||
timestamp=time.time(),
|
||||
tool_name="tool",
|
||||
server_name="server",
|
||||
arguments={"key": "value"},
|
||||
success=True,
|
||||
duration_ms=100.0
|
||||
)
|
||||
tool_call_tracer.record(record)
|
||||
|
||||
# 3. 实现处理方法
|
||||
async def _handle_newcmd(self, arg: str = None):
|
||||
# 处理逻辑
|
||||
await self.send_text("结果")
|
||||
return (True, None, True)
|
||||
# 查询记录
|
||||
recent = tool_call_tracer.get_recent(10)
|
||||
by_tool = tool_call_tracer.get_by_tool("tool_name")
|
||||
```
|
||||
|
||||
### 添加新配置项
|
||||
### 调用缓存
|
||||
|
||||
```python
|
||||
# 1. config_schema 添加
|
||||
"settings": {
|
||||
"new_option": ConfigField(
|
||||
type=bool,
|
||||
default=False,
|
||||
description="新选项说明",
|
||||
label="🆕 新选项",
|
||||
order=50,
|
||||
),
|
||||
}
|
||||
from plugin import tool_call_cache
|
||||
|
||||
# 2. 在 __init__ 或相应方法中读取
|
||||
new_option = settings.get("new_option", False)
|
||||
# 配置缓存
|
||||
tool_call_cache.configure(
|
||||
enabled=True,
|
||||
ttl=300, # 秒
|
||||
max_entries=200,
|
||||
exclude_tools="mcp_*_time_*" # 排除模式
|
||||
)
|
||||
|
||||
# 使用缓存
|
||||
cached = tool_call_cache.get("tool_name", {"param": "value"})
|
||||
if cached is None:
|
||||
result = await call_tool(...)
|
||||
tool_call_cache.set("tool_name", {"param": "value"}, result)
|
||||
```
|
||||
|
||||
### 添加新的全局模块
|
||||
### 权限控制
|
||||
|
||||
```python
|
||||
# 1. 定义数据类和管理类
|
||||
@dataclass
|
||||
class NewRecord:
|
||||
...
|
||||
from plugin import permission_checker
|
||||
|
||||
class NewManager:
|
||||
def configure(self, ...): ...
|
||||
def do_something(self, ...): ...
|
||||
# 配置权限
|
||||
permission_checker.configure(
|
||||
enabled=True,
|
||||
default_mode="allow_all", # 或 "deny_all"
|
||||
rules_json='[{"tool": "mcp_*_delete_*", "denied": ["qq:123:group"]}]',
|
||||
quick_deny_groups="123456789",
|
||||
quick_allow_users="111111111"
|
||||
)
|
||||
|
||||
new_manager = NewManager()
|
||||
|
||||
# 2. 在 MCPBridgePlugin.__init__ 中配置
|
||||
new_manager.configure(...)
|
||||
|
||||
# 3. 在 MCPToolProxy.execute() 中使用
|
||||
result = new_manager.do_something(...)
|
||||
# 检查权限
|
||||
allowed = permission_checker.check(
|
||||
tool_name="mcp_server_delete",
|
||||
chat_id="123456",
|
||||
user_id="789",
|
||||
is_group=True
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
### 断路器模式
|
||||
|
||||
## 调试
|
||||
MCP 客户端内置断路器,故障服务器快速失败:
|
||||
|
||||
- 连续失败 N 次后熔断
|
||||
- 熔断期间直接返回错误
|
||||
- 定期尝试恢复
|
||||
|
||||
## 扩展开发
|
||||
|
||||
### 添加新的传输类型
|
||||
|
||||
1. 在 `mcp_client.py` 中添加 `TransportType` 枚举值
|
||||
2. 实现对应的连接逻辑
|
||||
3. 更新 `_create_transport()` 方法
|
||||
|
||||
### 添加新的工具类型
|
||||
|
||||
1. 继承 `BaseTool` 创建新类
|
||||
2. 在 `get_plugin_components()` 中注册
|
||||
3. 实现 `execute()` 方法
|
||||
|
||||
### 添加新的命令
|
||||
|
||||
1. 在 `MCPStatusCommand.execute()` 中添加新的 action 分支
|
||||
2. 或创建新的 `BaseCommand` 子类
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 日志级别
|
||||
|
||||
```python
|
||||
# 导入
|
||||
from plugins.MCPBridgePlugin.mcp_client import mcp_manager
|
||||
from plugins.MCPBridgePlugin.plugin import tool_call_tracer, tool_call_cache, permission_checker
|
||||
from src.common.logger import get_logger
|
||||
logger = get_logger("mcp_bridge_plugin")
|
||||
|
||||
# 检查状态
|
||||
mcp_manager.get_status()
|
||||
mcp_manager.get_all_stats()
|
||||
|
||||
# 追踪记录
|
||||
tool_call_tracer.get_recent(10)
|
||||
|
||||
# 缓存状态
|
||||
tool_call_cache.get_stats()
|
||||
|
||||
# 手动调用
|
||||
result = await mcp_manager.call_tool("mcp_server_tool", {"arg": "value"})
|
||||
logger.debug("详细调试信息")
|
||||
logger.info("一般信息")
|
||||
logger.warning("警告")
|
||||
logger.error("错误")
|
||||
```
|
||||
|
||||
---
|
||||
### 常用调试命令
|
||||
|
||||
## 依赖
|
||||
```bash
|
||||
/mcp # 查看状态
|
||||
/mcp tools # 查看工具列表
|
||||
/mcp trace # 查看调用记录
|
||||
/mcp cache # 查看缓存状态
|
||||
/mcp chain # 查看工具链
|
||||
```
|
||||
|
||||
- MaiBot >= 0.11.6
|
||||
- Python >= 3.10
|
||||
- mcp >= 1.0.0
|
||||
## 更新日志
|
||||
|
||||
## 许可证
|
||||
见 `plugins/MaiBot_MCPBridgePlugin/CHANGELOG.md`
|
||||
|
||||
AGPL-3.0
|
||||
## 开发约定
|
||||
|
||||
- 本仓库不提交测试脚本/临时复现文件;如需本地验证,可自行在工作区创建未跟踪文件(建议放到 `.local/` 并加入 `.gitignore`)。
|
||||
|
|
|
|||
|
|
@ -23,21 +23,19 @@ cp config.example.toml config.toml
|
|||
|
||||
### 2. 添加服务器
|
||||
|
||||
编辑 `config.toml`,在 `[servers]` 的 `list` 中添加服务器:
|
||||
编辑 `config.toml`,在 `[servers]` 的 `claude_config_json` 中填写 Claude Desktop 的 `mcpServers` JSON:
|
||||
|
||||
**免费服务器:**
|
||||
```json
|
||||
{"name": "time", "enabled": true, "transport": "streamable_http", "url": "https://mcp.api-inference.modelscope.cn/server/mcp-server-time"}
|
||||
```
|
||||
|
||||
**带鉴权的服务器(v1.4.2):**
|
||||
```json
|
||||
{"name": "my-server", "enabled": true, "transport": "streamable_http", "url": "https://mcp.xxx.com/mcp", "headers": {"Authorization": "Bearer 你的密钥"}}
|
||||
```
|
||||
|
||||
**本地服务器(需要 uvx):**
|
||||
```json
|
||||
{"name": "fetch", "enabled": true, "transport": "stdio", "command": "uvx", "args": ["mcp-server-fetch"]}
|
||||
```toml
|
||||
[servers]
|
||||
claude_config_json = '''
|
||||
{
|
||||
"mcpServers": {
|
||||
"time": { "transport": "streamable_http", "url": "https://mcp.api-inference.modelscope.cn/server/mcp-server-time" },
|
||||
"my-server": { "transport": "streamable_http", "url": "https://mcp.xxx.com/mcp", "headers": { "Authorization": "Bearer 你的密钥" } },
|
||||
"fetch": { "command": "uvx", "args": ["mcp-server-fetch"] }
|
||||
}
|
||||
}
|
||||
'''
|
||||
```
|
||||
|
||||
### 3. 启动
|
||||
|
|
@ -67,8 +65,11 @@ cp config.example.toml config.toml
|
|||
| `/mcp cache` | 查看缓存状态 |
|
||||
| `/mcp perm` | 查看权限配置 |
|
||||
| `/mcp import <json>` | 🆕 导入 Claude Desktop 配置 |
|
||||
| `/mcp export [claude]` | 🆕 导出配置 |
|
||||
| `/mcp export` | 🆕 导出配置 |
|
||||
| `/mcp search <关键词>` | 🆕 搜索工具 |
|
||||
| `/mcp chain` | 🆕 查看工具链 |
|
||||
| `/mcp chain <名称>` | 🆕 查看工具链详情 |
|
||||
| `/mcp chain test <名称> <参数>` | 🆕 测试执行工具链 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -80,25 +81,18 @@ cp config.example.toml config.toml
|
|||
- 🔄 自动重试、心跳检测、断线重连
|
||||
- 🖥️ WebUI 完整配置支持
|
||||
|
||||
### v1.7.0 新增
|
||||
- ⚡ **断路器模式** - 故障服务器快速失败,避免拖慢整体响应
|
||||
- 🔄 **状态实时刷新** - WebUI 自动更新连接状态(可配置间隔)
|
||||
- 🔍 **工具搜索** - `/mcp search <关键词>` 快速查找工具
|
||||
|
||||
### v1.6.0 新增
|
||||
- 📥 **配置导入** - 从 Claude Desktop 格式一键导入
|
||||
- 📤 **配置导出** - 导出为 Claude Desktop / Kiro / MaiBot 格式
|
||||
|
||||
### v1.4.0 新增
|
||||
- 🚫 **工具禁用** - WebUI 直接禁用不想用的工具
|
||||
- 🔍 **调用追踪** - 记录每次调用详情,便于调试
|
||||
- 🗄️ **调用缓存** - 相同请求自动缓存
|
||||
- 🔐 **权限控制** - 按群/用户限制工具使用
|
||||
### 双轨制架构
|
||||
- 🔄 **ReAct(软流程)**:LLM 自主决策,多轮动态调用 MCP 工具(适合探索式场景)
|
||||
- 🔗 **Workflow(硬流程/工具链)**:用户预定义步骤顺序与参数传递(适合可控可复用场景)
|
||||
|
||||
### 高级功能
|
||||
- 📦 Resources 支持(实验性)
|
||||
- 📝 Prompts 支持(实验性)
|
||||
- 🔄 结果后处理(LLM 摘要提炼)
|
||||
- 🔍 调用追踪 / 🗄️ 调用缓存 / 🔐 权限控制 / 🚫 工具禁用
|
||||
|
||||
### 更新日志
|
||||
- 见 `plugins/MaiBot_MCPBridgePlugin/CHANGELOG.md`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -107,26 +101,26 @@ cp config.example.toml config.toml
|
|||
### 服务器配置
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "服务器名",
|
||||
"enabled": true,
|
||||
"transport": "streamable_http",
|
||||
"url": "https://..."
|
||||
{
|
||||
"mcpServers": {
|
||||
"server_name": {
|
||||
"transport": "streamable_http",
|
||||
"url": "https://..."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `name` | 服务器名称(唯一) |
|
||||
| `enabled` | 是否启用 |
|
||||
| `mcpServers.<name>` | 服务器名称(唯一) |
|
||||
| `enabled` | 是否启用(可选,默认 true) |
|
||||
| `transport` | `stdio` / `sse` / `http` / `streamable_http` |
|
||||
| `url` | 远程服务器地址 |
|
||||
| `headers` | 🆕 鉴权头(如 `{"Authorization": "Bearer xxx"}`) |
|
||||
| `command` / `args` | 本地服务器启动命令 |
|
||||
|
||||
### 权限控制(v1.4.0)
|
||||
### 权限控制
|
||||
|
||||
**快捷配置(推荐):**
|
||||
```toml
|
||||
|
|
@ -178,7 +172,7 @@ cache_exclude_tools = "mcp_*_time_*"
|
|||
|
||||
---
|
||||
|
||||
## 📥 配置导入导出(v1.6.0)
|
||||
## 📥 配置导入导出(Claude mcpServers)
|
||||
|
||||
### 从 Claude Desktop 导入
|
||||
|
||||
|
|
@ -190,16 +184,13 @@ cache_exclude_tools = "mcp_*_time_*"
|
|||
|
||||
支持的格式:
|
||||
- Claude Desktop 格式(`mcpServers` 对象)
|
||||
- Kiro MCP 格式
|
||||
- MaiBot 格式(数组)
|
||||
- 兼容旧版:MaiBot servers 列表数组(将自动迁移为 `mcpServers`)
|
||||
|
||||
### 导出配置
|
||||
|
||||
```
|
||||
/mcp export # 导出为 Claude Desktop 格式(默认)
|
||||
/mcp export claude # 导出为 Claude Desktop 格式
|
||||
/mcp export kiro # 导出为 Kiro MCP 格式
|
||||
/mcp export maibot # 导出为 MaiBot 格式
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
|
@ -209,6 +200,152 @@ cache_exclude_tools = "mcp_*_time_*"
|
|||
|
||||
---
|
||||
|
||||
## 🔗 Workflow(硬流程/工具链)
|
||||
|
||||
工具链允许你将多个 MCP 工具按顺序执行,后续工具可以使用前序工具的输出作为输入。
|
||||
|
||||
### 1 分钟上手(推荐 WebUI)
|
||||
1. 先完成 MCP 服务器配置并 `/mcp reconnect`
|
||||
2. 发送 `/mcp tools`,复制你要用的工具名
|
||||
3. 打开 WebUI → 「Workflow(硬流程/工具链)」→ 用“快速添加”表单填入:
|
||||
- 名称/描述
|
||||
- 输入参数(每行 `参数名=描述`)
|
||||
- 执行步骤(每行 `工具名|参数JSON|输出键`)
|
||||
4. 在“确认添加”中输入 `ADD` 并保存
|
||||
|
||||
### 快速添加工具链(推荐)
|
||||
|
||||
在 WebUI 的「工具链」配置区,使用表单快速添加:
|
||||
|
||||
1. **名称**: 填写工具链名称(英文,如 `search_and_detail`)
|
||||
2. **描述**: 填写工具链用途(供 LLM 理解何时使用)
|
||||
3. **输入参数**: 每行一个,格式 `参数名=描述`
|
||||
```
|
||||
query=搜索关键词
|
||||
max_results=最大结果数
|
||||
```
|
||||
4. **执行步骤**: 每行一个,格式 `工具名|参数JSON|输出键`
|
||||
```
|
||||
mcp_server_search|{"keyword":"${input.query}"}|search_result
|
||||
mcp_server_detail|{"id":"${prev}"}|
|
||||
```
|
||||
5. **确认添加**: 输入 `ADD` 并保存
|
||||
|
||||
### JSON 配置方式
|
||||
|
||||
也可以直接在「工具链列表」中编写 JSON:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "search_and_detail",
|
||||
"description": "先搜索模组,再获取详情",
|
||||
"input_params": {
|
||||
"query": "搜索关键词"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"tool_name": "mcp_mcmod_search_mod",
|
||||
"args_template": {"keyword": "${input.query}", "limit": 1},
|
||||
"output_key": "search_result",
|
||||
"description": "搜索模组"
|
||||
},
|
||||
{
|
||||
"tool_name": "mcp_mcmod_get_mod_detail",
|
||||
"args_template": {"mod_id": "${prev}"},
|
||||
"description": "获取详情"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 变量替换
|
||||
|
||||
| 变量格式 | 说明 |
|
||||
|---------|------|
|
||||
| `${input.参数名}` | 用户输入的参数 |
|
||||
| `${step.输出键}` | 某个步骤的输出(通过 `output_key` 指定) |
|
||||
| `${prev}` | 上一步的输出 |
|
||||
| `${prev.字段}` | 上一步输出(JSON)的某个字段 |
|
||||
| `${step.geo.return.0.location}` | 数组下标访问(dot) |
|
||||
| `${step.geo.return[0].location}` | 数组下标访问([]) |
|
||||
| `${step.geo['return'][0]['location']}` | bracket 写法(最通用) |
|
||||
|
||||
### 工具链字段说明
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `name` | 工具链名称,将生成 `chain_xxx` 工具 |
|
||||
| `description` | 描述,供 LLM 理解何时使用 |
|
||||
| `input_params` | 输入参数定义 `{参数名: 描述}` |
|
||||
| `steps` | 执行步骤数组 |
|
||||
| `steps[].tool_name` | 要调用的工具名 |
|
||||
| `steps[].args_template` | 参数模板,支持变量替换 |
|
||||
| `steps[].output_key` | 输出存储键名(可选) |
|
||||
| `steps[].optional` | 是否可选,失败时继续执行(默认 false) |
|
||||
|
||||
### 命令
|
||||
|
||||
```bash
|
||||
/mcp chain # 查看所有工具链
|
||||
/mcp chain list # 列出工具链
|
||||
/mcp chain <名称> # 查看详情
|
||||
/mcp chain test <名称> {"query": "JEI"} # 测试执行
|
||||
/mcp chain reload # 重新加载配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 双轨制架构
|
||||
|
||||
MCP 桥接插件支持两种工具调用模式,可根据场景选择:
|
||||
|
||||
### ReAct 软流程
|
||||
|
||||
LLM 自主决策的多轮工具调用模式,适合复杂、不确定的场景。
|
||||
|
||||
**工作原理:**
|
||||
1. 用户提问 → LLM 分析需要什么信息
|
||||
2. LLM 选择调用工具 → 获取结果
|
||||
3. LLM 观察结果 → 决定是否需要更多信息
|
||||
4. 重复 2-3 直到信息足够 → 生成最终回答
|
||||
|
||||
**启用方式:**
|
||||
在 WebUI「ReAct (软流程)」配置区启用,MCP 工具将自动注册到 MaiBot 的记忆检索 ReAct 系统。
|
||||
|
||||
**适用场景:**
|
||||
- 复杂问题需要多步推理
|
||||
- 不确定需要调用哪些工具
|
||||
- 需要根据中间结果动态调整
|
||||
|
||||
### Workflow 硬流程
|
||||
|
||||
用户预定义的工作流,固定执行顺序,适合可靠、可控的场景。
|
||||
|
||||
**工作原理:**
|
||||
1. 用户定义步骤顺序和参数传递
|
||||
2. 按顺序执行每个步骤
|
||||
3. 后续步骤可使用前序步骤的输出
|
||||
4. 返回最终结果
|
||||
|
||||
**适用场景:**
|
||||
- 流程固定、可预测
|
||||
- 需要可靠、可重复的执行
|
||||
- 希望精确控制工具调用顺序
|
||||
|
||||
### 对比
|
||||
|
||||
| 特性 | ReAct 软流程 | Workflow 硬流程 |
|
||||
|------|-------------|----------------|
|
||||
| 决策者 | LLM 自主决策 | 用户预定义 |
|
||||
| 灵活性 | 高,动态调整 | 低,固定流程 |
|
||||
| 可预测性 | 低 | 高 |
|
||||
| 适用场景 | 复杂、探索性任务 | 固定、重复性任务 |
|
||||
| 配置方式 | 启用即可 | 需要定义步骤 |
|
||||
|
||||
---
|
||||
|
||||
## 📋 依赖
|
||||
|
||||
- MaiBot >= 0.11.6
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"manifest_version": 1,
|
||||
"name": "MCP桥接插件",
|
||||
"version": "1.7.0",
|
||||
"version": "2.0.0",
|
||||
"description": "将 MCP (Model Context Protocol) 服务器的工具桥接到 MaiBot,使麦麦能够调用外部 MCP 工具",
|
||||
"author": {
|
||||
"name": "CharTyr",
|
||||
|
|
@ -26,7 +26,10 @@
|
|||
"permissions",
|
||||
"import",
|
||||
"export",
|
||||
"claude-desktop"
|
||||
"claude-desktop",
|
||||
"workflow",
|
||||
"react",
|
||||
"agent"
|
||||
],
|
||||
"categories": [
|
||||
"工具扩展",
|
||||
|
|
@ -51,9 +54,13 @@
|
|||
"调用链路追踪",
|
||||
"工具调用缓存(LRU)",
|
||||
"工具权限控制(群/用户级别)",
|
||||
"配置导入导出(Claude Desktop / Kiro 格式)",
|
||||
"配置导入导出(Claude Desktop mcpServers)",
|
||||
"断路器模式(故障快速失败)",
|
||||
"状态实时刷新"
|
||||
"状态实时刷新",
|
||||
"Workflow 硬流程(顺序执行多个工具)",
|
||||
"Workflow 快速添加(表单式配置)",
|
||||
"ReAct 软流程(LLM 自主多轮调用)",
|
||||
"双轨制架构(软流程 + 硬流程)"
|
||||
]
|
||||
},
|
||||
"id": "MaiBot Community.MCPBridgePlugin"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# MCP桥接插件 v1.7.0 - 配置文件示例
|
||||
# MCP桥接插件 - 配置文件示例
|
||||
# 将 MCP (Model Context Protocol) 服务器的工具桥接到 MaiBot
|
||||
#
|
||||
# 使用方法:复制此文件为 config.toml,然后根据需要修改配置
|
||||
|
|
@ -24,31 +24,123 @@
|
|||
#
|
||||
# ============================================================
|
||||
|
||||
# ============================================================
|
||||
# 🔌 MCP 服务器配置
|
||||
# ============================================================
|
||||
#
|
||||
# ⚠️ 重要:配置格式(Claude Desktop 规范)
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# 统一使用 Claude Desktop 的 mcpServers JSON。
|
||||
#
|
||||
# claude_config_json 的内容应为 JSON 对象:
|
||||
# {
|
||||
# "mcpServers": {
|
||||
# "server_name": { ...server config... },
|
||||
# "another": { ... }
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# 每个服务器支持字段:
|
||||
# transport - 传输方式: "stdio" / "sse" / "http" / "streamable_http"(可选)
|
||||
# url - 服务器地址(sse/http/streamable_http 模式)
|
||||
# command - 启动命令(stdio 模式,如 "npx" / "uvx")
|
||||
# args - 命令参数数组(stdio 模式)
|
||||
# env - 环境变量对象(stdio 模式,可选)
|
||||
# headers - 鉴权头(可选,如 {"Authorization": "Bearer xxx"})
|
||||
# enabled - 是否启用(可选,默认 true)
|
||||
# post_process - 服务器级别后处理配置(可选)
|
||||
#
|
||||
# ============================================================
|
||||
|
||||
[servers]
|
||||
claude_config_json = '''
|
||||
{
|
||||
"mcpServers": {
|
||||
"time-mcp-server": {
|
||||
"enabled": false,
|
||||
"transport": "streamable_http",
|
||||
"url": "https://mcp.api-inference.modelscope.cn/server/mcp-server-time"
|
||||
},
|
||||
"my-auth-server": {
|
||||
"enabled": false,
|
||||
"transport": "streamable_http",
|
||||
"url": "https://mcp.api-inference.modelscope.net/xxxxxx/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ms-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
},
|
||||
"fetch-local": {
|
||||
"enabled": false,
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
# ============================================================
|
||||
# 插件基本信息
|
||||
# ============================================================
|
||||
[plugin]
|
||||
name = "mcp_bridge_plugin"
|
||||
version = "1.7.0"
|
||||
config_version = "1.7.0"
|
||||
version = "2.0.0"
|
||||
config_version = "2.0.0"
|
||||
enabled = false # 默认禁用,在 WebUI 中启用
|
||||
|
||||
# ============================================================
|
||||
# 🆕 v1.5.4 快速入门(只读,WebUI 显示)
|
||||
# Workflow(硬流程/工具链)
|
||||
# ============================================================
|
||||
#
|
||||
# 作用:把多个工具按顺序执行;后续步骤可引用前序输出。
|
||||
#
|
||||
# ✅ 推荐配置方式:WebUI「Workflow(硬流程/工具链)」里用“快速添加”表单。
|
||||
# ✅ 也可以直接写 chains_list(JSON 数组)。
|
||||
#
|
||||
# 变量替换:
|
||||
# ${input.xxx} - 用户输入
|
||||
# ${step.<output_key>} - 指定步骤输出(需设置 output_key)
|
||||
# ${prev} - 上一步输出
|
||||
# ${prev.字段} - 上一步输出(JSON)的字段
|
||||
# ${step.geo.return.0.location} - 数组/下标访问(dot)
|
||||
# ${step.geo.return[0].location} - 数组/下标访问([])
|
||||
# ${step.geo['return'][0]['location']} - bracket 写法
|
||||
#
|
||||
# ============================================================
|
||||
[guide]
|
||||
# 🚀 快速入门 - 三步开始使用
|
||||
quick_start = "1. 从下方链接获取 MCP 服务器 2. 在「快速添加」填写信息 3. 保存后发送 /mcp reconnect"
|
||||
|
||||
# 🌐 获取 MCP 服务器 - 复制链接到浏览器打开,获取免费 MCP 服务器
|
||||
# 魔搭 ModelScope 国内免费推荐,复制服务器 URL 到「快速添加」即可
|
||||
mcp_sources = "https://modelscope.cn/mcp (魔搭·推荐) | https://smithery.ai | https://glama.ai | https://mcp.so"
|
||||
[tool_chains]
|
||||
chains_enabled = true
|
||||
|
||||
# 📝 配置示例 - 复制到服务器列表可直接使用(免费时间服务器)
|
||||
example_config = '{"name": "time", "enabled": true, "transport": "streamable_http", "url": "https://mcp.api-inference.modelscope.cn/server/mcp-server-time"}'
|
||||
chains_list = '''
|
||||
[
|
||||
{
|
||||
"name": "search_and_detail",
|
||||
"description": "先搜索,再根据结果获取详情",
|
||||
"input_params": { "query": "搜索关键词" },
|
||||
"steps": [
|
||||
{ "tool_name": "把这里替换成你的搜索工具名", "args_template": { "keyword": "${input.query}" }, "output_key": "search" },
|
||||
{ "tool_name": "把这里替换成你的详情工具名", "args_template": { "id": "${prev}" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
'''
|
||||
|
||||
# ============================================================
|
||||
# 全局设置
|
||||
# ReAct(软流程)
|
||||
# ============================================================
|
||||
#
|
||||
# 作用:把 MCP 工具注册到 MaiBot 的 ReAct 系统,LLM 可自主多轮调用。
|
||||
#
|
||||
# 注意:ReAct 适合“探索式/不确定”场景;Workflow 适合“固定/可控”场景。
|
||||
#
|
||||
# ============================================================
|
||||
|
||||
[react]
|
||||
react_enabled = false
|
||||
filter_mode = "whitelist" # whitelist / blacklist
|
||||
tool_filter = "" # 每行一个工具名,支持通配符 *
|
||||
|
||||
# ============================================================
|
||||
# 全局设置(高级设置建议保持默认)
|
||||
# ============================================================
|
||||
[settings]
|
||||
# 🏷️ 工具前缀 - 用于区分 MCP 工具和原生工具
|
||||
|
|
@ -75,13 +167,6 @@ heartbeat_enabled = true
|
|||
# 💓 心跳间隔(秒)- 建议 30-120 秒
|
||||
heartbeat_interval = 60.0
|
||||
|
||||
# 🧠 智能心跳 - 根据服务器稳定性自动调整心跳间隔(v1.5.2)
|
||||
# 稳定服务器逐渐增加间隔,断开的服务器缩短间隔
|
||||
heartbeat_adaptive = true
|
||||
|
||||
# 📈 最大间隔倍数 - 稳定服务器心跳间隔最高可达 基准间隔 × 此值(v1.5.3)
|
||||
heartbeat_max_multiplier = 3.0
|
||||
|
||||
# 🔄 自动重连 - 检测到断开时自动尝试重连
|
||||
auto_reconnect = true
|
||||
|
||||
|
|
@ -89,16 +174,7 @@ auto_reconnect = true
|
|||
max_reconnect_attempts = 3
|
||||
|
||||
# ============================================================
|
||||
# v1.7.0 状态实时刷新
|
||||
# ============================================================
|
||||
# 📊 启用状态实时刷新 - 定期更新 WebUI 状态显示
|
||||
status_refresh_enabled = true
|
||||
|
||||
# 📊 状态刷新间隔(秒)- 值越小刷新越频繁,但会增加少量磁盘写入
|
||||
status_refresh_interval = 10.0
|
||||
|
||||
# ============================================================
|
||||
# v1.2.0 高级功能(实验性)
|
||||
# 高级功能(实验性)
|
||||
# ============================================================
|
||||
# 📦 启用 Resources - 允许读取 MCP 服务器提供的资源
|
||||
enable_resources = false
|
||||
|
|
@ -107,7 +183,7 @@ enable_resources = false
|
|||
enable_prompts = false
|
||||
|
||||
# ============================================================
|
||||
# v1.3.0 结果后处理功能
|
||||
# 结果后处理功能
|
||||
# ============================================================
|
||||
# 当 MCP 工具返回的内容过长时,使用 LLM 对结果进行摘要提炼
|
||||
|
||||
|
|
@ -117,13 +193,13 @@ post_process_enabled = false
|
|||
# 📏 后处理阈值(字符数)- 结果长度超过此值才触发后处理
|
||||
post_process_threshold = 500
|
||||
|
||||
# <EFBFBD> 后处理输e出限制 - LLM 摘要输出的最大 token 数
|
||||
# 🔢 后处理输出限制 - LLM 摘要输出的最大 token 数
|
||||
post_process_max_tokens = 500
|
||||
|
||||
# 🤖 后处理模型(可选)- 留空则使用 utils 模型组
|
||||
post_process_model = ""
|
||||
|
||||
# <EFBFBD> 后处理提示词模板-
|
||||
# 🧠 后处理提示词模板
|
||||
post_process_prompt = '''用户问题:{query}
|
||||
|
||||
工具返回内容:
|
||||
|
|
@ -132,7 +208,7 @@ post_process_prompt = '''用户问题:{query}
|
|||
请从上述内容中提取与用户问题最相关的关键信息,简洁准确地输出:'''
|
||||
|
||||
# ============================================================
|
||||
# 🆕 v1.4.0 调用链路追踪
|
||||
# 调用链路追踪
|
||||
# ============================================================
|
||||
# 记录工具调用详情,便于调试和分析
|
||||
|
||||
|
|
@ -140,14 +216,14 @@ post_process_prompt = '''用户问题:{query}
|
|||
trace_enabled = true
|
||||
|
||||
# 📊 追踪记录上限 - 内存中保留的最大记录数
|
||||
trace_max_records = 100
|
||||
trace_max_records = 50
|
||||
|
||||
# 📝 追踪日志文件 - 是否将追踪记录写入日志文件
|
||||
# 启用后记录写入 plugins/MaiBot_MCPBridgePlugin/logs/trace.jsonl
|
||||
trace_log_enabled = false
|
||||
|
||||
# ============================================================
|
||||
# 🆕 v1.4.0 工具调用缓存
|
||||
# 工具调用缓存
|
||||
# ============================================================
|
||||
# 缓存相同参数的调用结果,减少重复请求
|
||||
|
||||
|
|
@ -160,7 +236,7 @@ cache_ttl = 300
|
|||
# 📦 最大缓存条目 - 超出后 LRU 淘汰
|
||||
cache_max_entries = 200
|
||||
|
||||
# <EFBFBD> 缓存排除列表 - 即不缓存的工具(每行一个,支持通配符 *)
|
||||
# 🚫 缓存排除列表 - 即不缓存的工具(每行一个,支持通配符 *)
|
||||
# 时间类、随机类工具建议排除
|
||||
cache_exclude_tools = '''
|
||||
mcp_*_time_*
|
||||
|
|
@ -168,7 +244,7 @@ mcp_*_random_*
|
|||
'''
|
||||
|
||||
# ============================================================
|
||||
# 🆕 v1.4.0 工具管理
|
||||
# 工具管理
|
||||
# ============================================================
|
||||
[tools]
|
||||
# 📋 工具清单(只读)- 启动后自动生成
|
||||
|
|
@ -184,48 +260,7 @@ tool_list = "(启动后自动生成)"
|
|||
disabled_tools = ""
|
||||
|
||||
# ============================================================
|
||||
# 🆕 v1.5.1 快速添加服务器
|
||||
# ============================================================
|
||||
# 表单式配置,无需手写 JSON
|
||||
[quick_add]
|
||||
# 📛 服务器名称 - 服务器唯一名称(英文,如 time-server)
|
||||
server_name = ""
|
||||
|
||||
# 📡 传输类型 - 远程服务器选 streamable_http/http/sse,本地选 stdio
|
||||
server_type = "streamable_http"
|
||||
|
||||
# 🌐 服务器 URL - 远程服务器必填(streamable_http/http/sse 类型)
|
||||
server_url = ""
|
||||
|
||||
# ⌨️ 启动命令 - stdio 类型必填(如 uvx、npx、python)
|
||||
server_command = ""
|
||||
|
||||
# 📝 命令参数 - stdio 类型使用,每行一个参数
|
||||
server_args = ""
|
||||
|
||||
# 🔑 鉴权头(可选)- JSON 格式,如 {"Authorization": "Bearer xxx"}
|
||||
server_headers = ""
|
||||
|
||||
# ============================================================
|
||||
# 🆕 v1.6.0 配置导入导出
|
||||
# ============================================================
|
||||
# 支持从 Claude Desktop / Kiro / MaiBot 格式导入导出
|
||||
[import_export]
|
||||
# 📥 导入配置 - 粘贴 Claude Desktop 或其他格式的 MCP 配置 JSON
|
||||
# 粘贴配置后点击保存,2秒内自动导入。查看下方「导入结果」确认状态
|
||||
import_config = ""
|
||||
|
||||
# 📋 导入结果(只读)- 显示导入操作的结果
|
||||
import_result = ""
|
||||
|
||||
# 📤 导出格式 - claude: Claude Desktop 格式 | kiro: Kiro MCP 格式 | maibot: 本插件格式
|
||||
export_format = "claude"
|
||||
|
||||
# 📤 导出结果(只读,可复制)- 点击保存后生成,可复制到 Claude Desktop 或其他支持 MCP 的应用
|
||||
export_result = "(点击保存后生成)"
|
||||
|
||||
# ============================================================
|
||||
# 🆕 v1.4.0 权限控制
|
||||
# 权限控制
|
||||
# ============================================================
|
||||
[permissions]
|
||||
# 🔐 启用权限控制 - 按群/用户限制工具使用
|
||||
|
|
@ -267,66 +302,6 @@ quick_allow_users = ""
|
|||
# '''
|
||||
perm_rules = "[]"
|
||||
|
||||
# ============================================================
|
||||
# 🔌 MCP 服务器配置
|
||||
# ============================================================
|
||||
#
|
||||
# ⚠️ 重要:JSON 格式说明
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# 服务器列表必须是 JSON 数组格式!
|
||||
#
|
||||
# ❌ 错误写法:
|
||||
# { "name": "server1", ... },
|
||||
# { "name": "server2", ... }
|
||||
#
|
||||
# ✅ 正确写法:
|
||||
# [
|
||||
# { "name": "server1", ... },
|
||||
# { "name": "server2", ... }
|
||||
# ]
|
||||
#
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# 每个服务器的配置字段:
|
||||
# name - 服务器名称(唯一标识)
|
||||
# enabled - 是否启用 (true/false)
|
||||
# transport - 传输方式: "stdio" / "sse" / "http" / "streamable_http"
|
||||
# url - 服务器地址(sse/http/streamable_http 模式必填)
|
||||
# headers - 🆕 鉴权头(可选,如 {"Authorization": "Bearer xxx"})
|
||||
# command - 启动命令(stdio 模式,如 "npx" 或 "uvx")
|
||||
# args - 命令参数数组(stdio 模式)
|
||||
# env - 环境变量对象(stdio 模式,可选)
|
||||
# post_process - 服务器级别后处理配置(可选)
|
||||
#
|
||||
# ============================================================
|
||||
|
||||
[servers]
|
||||
list = '''
|
||||
[
|
||||
{
|
||||
"name": "time-mcp-server",
|
||||
"enabled": false,
|
||||
"transport": "streamable_http",
|
||||
"url": "https://mcp.api-inference.modelscope.cn/server/mcp-server-time"
|
||||
},
|
||||
{
|
||||
"name": "my-auth-server",
|
||||
"enabled": false,
|
||||
"transport": "streamable_http",
|
||||
"url": "https://mcp.api-inference.modelscope.net/xxxxxx/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ms-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fetch-local",
|
||||
"enabled": false,
|
||||
"transport": "stdio",
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
}
|
||||
]
|
||||
'''
|
||||
|
||||
# ============================================================
|
||||
# 状态显示(只读)
|
||||
# ============================================================
|
||||
|
|
|
|||
|
|
@ -1,436 +0,0 @@
|
|||
"""
|
||||
MCP 配置格式转换模块 v1.0.0
|
||||
|
||||
支持的格式:
|
||||
- Claude Desktop (claude_desktop_config.json)
|
||||
- Kiro MCP (mcp.json)
|
||||
- MaiBot MCP Bridge Plugin (本插件格式)
|
||||
|
||||
转换规则:
|
||||
- stdio: command + args + env
|
||||
- sse/http/streamable_http: url + headers
|
||||
"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConversionResult:
|
||||
"""转换结果"""
|
||||
|
||||
success: bool
|
||||
servers: List[Dict[str, Any]] = field(default_factory=list)
|
||||
errors: List[str] = field(default_factory=list)
|
||||
warnings: List[str] = field(default_factory=list)
|
||||
skipped: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class ConfigConverter:
|
||||
"""MCP 配置格式转换器"""
|
||||
|
||||
# transport 类型映射 (外部格式 -> 内部格式)
|
||||
TRANSPORT_MAP_IN = {
|
||||
"sse": "sse",
|
||||
"http": "http",
|
||||
"streamable-http": "streamable_http",
|
||||
"streamable_http": "streamable_http",
|
||||
"streamable-http": "streamable_http",
|
||||
"stdio": "stdio",
|
||||
}
|
||||
|
||||
# 支持的 transport 字段名(有些格式用 type 而不是 transport)
|
||||
TRANSPORT_FIELD_NAMES = ["transport", "type"]
|
||||
|
||||
# transport 类型映射 (内部格式 -> Claude 格式)
|
||||
TRANSPORT_MAP_OUT = {
|
||||
"sse": "sse",
|
||||
"http": "http",
|
||||
"streamable_http": "streamable-http",
|
||||
"stdio": "stdio",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def detect_format(cls, config: Dict[str, Any]) -> Optional[str]:
|
||||
"""检测配置格式类型
|
||||
|
||||
Returns:
|
||||
"claude": Claude Desktop 格式 (mcpServers 对象)
|
||||
"kiro": Kiro MCP 格式 (mcpServers 对象,与 Claude 相同)
|
||||
"maibot": MaiBot 插件格式 (数组)
|
||||
None: 无法识别
|
||||
"""
|
||||
if isinstance(config, list):
|
||||
# 数组格式,检查是否是 MaiBot 格式
|
||||
if len(config) == 0:
|
||||
return "maibot"
|
||||
if isinstance(config[0], dict) and "name" in config[0]:
|
||||
return "maibot"
|
||||
return None
|
||||
|
||||
if isinstance(config, dict):
|
||||
# 对象格式
|
||||
if "mcpServers" in config:
|
||||
return "claude" # Claude 和 Kiro 格式相同
|
||||
# 可能是单个服务器配置
|
||||
if "name" in config:
|
||||
return "maibot_single"
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def parse_json_safe(cls, json_str: str) -> Tuple[Optional[Any], Optional[str]]:
|
||||
"""安全解析 JSON 字符串
|
||||
|
||||
Returns:
|
||||
(解析结果, 错误信息)
|
||||
"""
|
||||
if not json_str or not json_str.strip():
|
||||
return None, "输入为空"
|
||||
|
||||
json_str = json_str.strip()
|
||||
|
||||
try:
|
||||
return json.loads(json_str), None
|
||||
except json.JSONDecodeError as e:
|
||||
# 尝试提供更友好的错误信息
|
||||
line = e.lineno
|
||||
col = e.colno
|
||||
return None, f"JSON 解析失败 (行 {line}, 列 {col}): {e.msg}"
|
||||
|
||||
@classmethod
|
||||
def validate_server_config(cls, name: str, config: Dict[str, Any]) -> Tuple[bool, Optional[str], List[str]]:
|
||||
"""验证单个服务器配置
|
||||
|
||||
Args:
|
||||
name: 服务器名称
|
||||
config: 服务器配置字典
|
||||
|
||||
Returns:
|
||||
(是否有效, 错误信息, 警告列表)
|
||||
"""
|
||||
warnings = []
|
||||
|
||||
if not isinstance(config, dict):
|
||||
return False, f"服务器 '{name}' 配置必须是对象", []
|
||||
|
||||
has_command = "command" in config
|
||||
has_url = "url" in config
|
||||
|
||||
# 必须有 command 或 url 之一
|
||||
if not has_command and not has_url:
|
||||
return False, f"服务器 '{name}' 缺少 'command' 或 'url' 字段", []
|
||||
|
||||
# 同时有 command 和 url 时给出警告
|
||||
if has_command and has_url:
|
||||
warnings.append(f"'{name}': 同时存在 command 和 url,将优先使用 stdio 模式")
|
||||
|
||||
# 验证 url 格式
|
||||
if has_url and not has_command:
|
||||
url = config.get("url", "")
|
||||
if not isinstance(url, str):
|
||||
return False, f"服务器 '{name}' 的 url 必须是字符串", []
|
||||
if not url.startswith(("http://", "https://")):
|
||||
warnings.append(f"'{name}': url 不是标准 HTTP(S) 地址")
|
||||
|
||||
# 验证 command 格式
|
||||
if has_command:
|
||||
command = config.get("command", "")
|
||||
if not isinstance(command, str):
|
||||
return False, f"服务器 '{name}' 的 command 必须是字符串", []
|
||||
if not command.strip():
|
||||
return False, f"服务器 '{name}' 的 command 不能为空", []
|
||||
|
||||
# 验证 args 格式
|
||||
if "args" in config:
|
||||
args = config.get("args")
|
||||
if not isinstance(args, list):
|
||||
return False, f"服务器 '{name}' 的 args 必须是数组", []
|
||||
for i, arg in enumerate(args):
|
||||
if not isinstance(arg, str):
|
||||
warnings.append(f"'{name}': args[{i}] 不是字符串,将自动转换")
|
||||
|
||||
# 验证 env 格式
|
||||
if "env" in config:
|
||||
env = config.get("env")
|
||||
if not isinstance(env, dict):
|
||||
return False, f"服务器 '{name}' 的 env 必须是对象", []
|
||||
|
||||
# 验证 headers 格式
|
||||
if "headers" in config:
|
||||
headers = config.get("headers")
|
||||
if not isinstance(headers, dict):
|
||||
return False, f"服务器 '{name}' 的 headers 必须是对象", []
|
||||
|
||||
# 验证 transport/type 格式
|
||||
transport_value = None
|
||||
for field_name in cls.TRANSPORT_FIELD_NAMES:
|
||||
if field_name in config:
|
||||
transport_value = config.get(field_name, "").lower()
|
||||
break
|
||||
if transport_value and transport_value not in cls.TRANSPORT_MAP_IN:
|
||||
warnings.append(f"'{name}': 未知的 transport 类型 '{transport_value}',将自动推断")
|
||||
|
||||
return True, None, warnings
|
||||
|
||||
@classmethod
|
||||
def convert_claude_server(cls, name: str, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""将单个 Claude 格式服务器配置转换为 MaiBot 格式
|
||||
|
||||
Args:
|
||||
name: 服务器名称
|
||||
config: Claude 格式的服务器配置
|
||||
|
||||
Returns:
|
||||
MaiBot 格式的服务器配置
|
||||
"""
|
||||
result = {
|
||||
"name": name,
|
||||
"enabled": True,
|
||||
}
|
||||
|
||||
has_command = "command" in config
|
||||
|
||||
if has_command:
|
||||
# stdio 模式
|
||||
result["transport"] = "stdio"
|
||||
result["command"] = config.get("command", "")
|
||||
|
||||
# 处理 args
|
||||
args = config.get("args", [])
|
||||
if args:
|
||||
# 确保所有 args 都是字符串
|
||||
result["args"] = [str(arg) for arg in args]
|
||||
|
||||
# 处理 env
|
||||
env = config.get("env", {})
|
||||
if env and isinstance(env, dict):
|
||||
result["env"] = env
|
||||
|
||||
else:
|
||||
# 远程模式 (sse/http/streamable_http)
|
||||
# 支持 transport 或 type 字段
|
||||
transport_raw = None
|
||||
for field_name in cls.TRANSPORT_FIELD_NAMES:
|
||||
if field_name in config:
|
||||
transport_raw = config.get(field_name, "").lower()
|
||||
break
|
||||
if not transport_raw:
|
||||
transport_raw = "sse"
|
||||
result["transport"] = cls.TRANSPORT_MAP_IN.get(transport_raw, "sse")
|
||||
result["url"] = config.get("url", "")
|
||||
|
||||
# 处理 headers
|
||||
headers = config.get("headers", {})
|
||||
if headers and isinstance(headers, dict):
|
||||
result["headers"] = headers
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def convert_maibot_server(cls, config: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
|
||||
"""将单个 MaiBot 格式服务器配置转换为 Claude 格式
|
||||
|
||||
Args:
|
||||
config: MaiBot 格式的服务器配置
|
||||
|
||||
Returns:
|
||||
(服务器名称, Claude 格式的服务器配置)
|
||||
"""
|
||||
name = config.get("name", "unnamed")
|
||||
result = {}
|
||||
|
||||
transport = config.get("transport", "stdio").lower()
|
||||
|
||||
if transport == "stdio":
|
||||
# stdio 模式
|
||||
result["command"] = config.get("command", "")
|
||||
|
||||
args = config.get("args", [])
|
||||
if args:
|
||||
result["args"] = args
|
||||
|
||||
env = config.get("env", {})
|
||||
if env:
|
||||
result["env"] = env
|
||||
|
||||
else:
|
||||
# 远程模式
|
||||
result["url"] = config.get("url", "")
|
||||
|
||||
# 转换 transport 名称
|
||||
claude_transport = cls.TRANSPORT_MAP_OUT.get(transport, "sse")
|
||||
if claude_transport != "sse": # sse 是默认值,可以省略
|
||||
result["transport"] = claude_transport
|
||||
|
||||
headers = config.get("headers", {})
|
||||
if headers:
|
||||
result["headers"] = headers
|
||||
|
||||
return name, result
|
||||
|
||||
@classmethod
|
||||
def from_claude_format(cls, config: Dict[str, Any], existing_names: Optional[set] = None) -> ConversionResult:
|
||||
"""从 Claude Desktop 格式转换为 MaiBot 格式
|
||||
|
||||
Args:
|
||||
config: Claude Desktop 配置 (包含 mcpServers 字段)
|
||||
existing_names: 已存在的服务器名称集合,用于跳过重复
|
||||
|
||||
Returns:
|
||||
ConversionResult
|
||||
"""
|
||||
result = ConversionResult(success=True)
|
||||
existing_names = existing_names or set()
|
||||
|
||||
# 检查格式
|
||||
if not isinstance(config, dict):
|
||||
result.success = False
|
||||
result.errors.append("配置必须是 JSON 对象")
|
||||
return result
|
||||
|
||||
mcp_servers = config.get("mcpServers", {})
|
||||
|
||||
if not isinstance(mcp_servers, dict):
|
||||
result.success = False
|
||||
result.errors.append("mcpServers 必须是对象")
|
||||
return result
|
||||
|
||||
if not mcp_servers:
|
||||
result.warnings.append("mcpServers 为空,没有服务器可导入")
|
||||
return result
|
||||
|
||||
# 转换每个服务器
|
||||
for name, srv_config in mcp_servers.items():
|
||||
# 检查名称是否已存在
|
||||
if name in existing_names:
|
||||
result.skipped.append(f"'{name}' (已存在)")
|
||||
continue
|
||||
|
||||
# 验证配置
|
||||
valid, error, warnings = cls.validate_server_config(name, srv_config)
|
||||
result.warnings.extend(warnings)
|
||||
|
||||
if not valid:
|
||||
result.errors.append(error)
|
||||
continue
|
||||
|
||||
# 转换配置
|
||||
try:
|
||||
converted = cls.convert_claude_server(name, srv_config)
|
||||
result.servers.append(converted)
|
||||
except Exception as e:
|
||||
result.errors.append(f"转换服务器 '{name}' 失败: {str(e)}")
|
||||
|
||||
# 如果有错误但也有成功的,仍然标记为成功(部分成功)
|
||||
if result.errors and not result.servers:
|
||||
result.success = False
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def to_claude_format(cls, servers: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""将 MaiBot 格式转换为 Claude Desktop 格式
|
||||
|
||||
Args:
|
||||
servers: MaiBot 格式的服务器列表
|
||||
|
||||
Returns:
|
||||
Claude Desktop 格式的配置
|
||||
"""
|
||||
mcp_servers = {}
|
||||
|
||||
for srv in servers:
|
||||
if not isinstance(srv, dict):
|
||||
continue
|
||||
|
||||
name, config = cls.convert_maibot_server(srv)
|
||||
mcp_servers[name] = config
|
||||
|
||||
return {"mcpServers": mcp_servers}
|
||||
|
||||
@classmethod
|
||||
def import_from_string(cls, json_str: str, existing_names: Optional[set] = None) -> ConversionResult:
|
||||
"""从 JSON 字符串导入配置
|
||||
|
||||
自动检测格式并转换为 MaiBot 格式
|
||||
|
||||
Args:
|
||||
json_str: JSON 字符串
|
||||
existing_names: 已存在的服务器名称集合
|
||||
|
||||
Returns:
|
||||
ConversionResult
|
||||
"""
|
||||
result = ConversionResult(success=True)
|
||||
existing_names = existing_names or set()
|
||||
|
||||
# 解析 JSON
|
||||
parsed, error = cls.parse_json_safe(json_str)
|
||||
if error:
|
||||
result.success = False
|
||||
result.errors.append(error)
|
||||
return result
|
||||
|
||||
# 检测格式
|
||||
fmt = cls.detect_format(parsed)
|
||||
|
||||
if fmt is None:
|
||||
result.success = False
|
||||
result.errors.append("无法识别的配置格式")
|
||||
return result
|
||||
|
||||
if fmt == "maibot":
|
||||
# 已经是 MaiBot 格式,直接验证并返回
|
||||
for srv in parsed:
|
||||
if not isinstance(srv, dict):
|
||||
result.warnings.append("跳过非对象元素")
|
||||
continue
|
||||
|
||||
name = srv.get("name", "")
|
||||
if not name:
|
||||
result.warnings.append("跳过缺少 name 的服务器")
|
||||
continue
|
||||
|
||||
if name in existing_names:
|
||||
result.skipped.append(f"'{name}' (已存在)")
|
||||
continue
|
||||
|
||||
result.servers.append(srv)
|
||||
|
||||
elif fmt == "maibot_single":
|
||||
# 单个 MaiBot 格式服务器
|
||||
name = parsed.get("name", "")
|
||||
if name in existing_names:
|
||||
result.skipped.append(f"'{name}' (已存在)")
|
||||
else:
|
||||
result.servers.append(parsed)
|
||||
|
||||
elif fmt in ("claude", "kiro"):
|
||||
# Claude/Kiro 格式
|
||||
return cls.from_claude_format(parsed, existing_names)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def export_to_string(cls, servers: List[Dict[str, Any]], format_type: str = "claude", pretty: bool = True) -> str:
|
||||
"""导出配置为 JSON 字符串
|
||||
|
||||
Args:
|
||||
servers: MaiBot 格式的服务器列表
|
||||
format_type: 导出格式 ("claude", "kiro", "maibot")
|
||||
pretty: 是否格式化输出
|
||||
|
||||
Returns:
|
||||
JSON 字符串
|
||||
"""
|
||||
indent = 2 if pretty else None
|
||||
|
||||
if format_type in ("claude", "kiro"):
|
||||
config = cls.to_claude_format(servers)
|
||||
else:
|
||||
config = servers
|
||||
|
||||
return json.dumps(config, ensure_ascii=False, indent=indent)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
"""Core helpers for MCP Bridge Plugin."""
|
||||
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Literal, Optional
|
||||
|
||||
|
||||
class ClaudeConfigError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
Transport = Literal["stdio", "sse", "http", "streamable_http"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClaudeMcpServer:
|
||||
name: str
|
||||
transport: Transport
|
||||
command: str = ""
|
||||
args: List[str] = field(default_factory=list)
|
||||
env: Dict[str, str] = field(default_factory=dict)
|
||||
url: str = ""
|
||||
headers: Dict[str, str] = field(default_factory=dict)
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
def _normalize_transport(value: Optional[str]) -> Transport:
|
||||
if not value:
|
||||
return "streamable_http"
|
||||
v = value.strip().lower().replace("-", "_")
|
||||
if v in ("streamable_http", "streamablehttp", "streamable"):
|
||||
return "streamable_http"
|
||||
if v in ("http",):
|
||||
return "http"
|
||||
if v in ("sse",):
|
||||
return "sse"
|
||||
if v in ("stdio",):
|
||||
return "stdio"
|
||||
raise ClaudeConfigError(f"unsupported transport: {value}")
|
||||
|
||||
|
||||
def _coerce_str_list(value: Any, field_name: str) -> List[str]:
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return [str(v) for v in value]
|
||||
raise ClaudeConfigError(f"{field_name} must be a list")
|
||||
|
||||
|
||||
def _coerce_str_dict(value: Any, field_name: str) -> Dict[str, str]:
|
||||
if value is None:
|
||||
return {}
|
||||
if isinstance(value, dict):
|
||||
return {str(k): str(v) for k, v in value.items()}
|
||||
raise ClaudeConfigError(f"{field_name} must be an object")
|
||||
|
||||
|
||||
def parse_claude_mcp_config(config_json: str) -> List[ClaudeMcpServer]:
|
||||
"""Parse Claude Desktop style MCP config JSON.
|
||||
|
||||
Supported:
|
||||
- Full object: {"mcpServers": {...}}
|
||||
- Direct mapping: {...} treated as mcpServers
|
||||
"""
|
||||
text = (config_json or "").strip()
|
||||
if not text:
|
||||
return []
|
||||
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ClaudeConfigError(f"invalid JSON: {e}") from e
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise ClaudeConfigError("config must be a JSON object")
|
||||
|
||||
servers_obj = data.get("mcpServers", data)
|
||||
if not isinstance(servers_obj, dict):
|
||||
raise ClaudeConfigError("mcpServers must be an object")
|
||||
|
||||
servers: List[ClaudeMcpServer] = []
|
||||
for name, raw in servers_obj.items():
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
raise ClaudeConfigError("server name must be a non-empty string")
|
||||
if not isinstance(raw, dict):
|
||||
raise ClaudeConfigError(f"server '{name}' must be an object")
|
||||
|
||||
enabled = bool(raw.get("enabled", True))
|
||||
command = str(raw.get("command", "") or "")
|
||||
url = str(raw.get("url", "") or "")
|
||||
args = _coerce_str_list(raw.get("args"), "args")
|
||||
env = _coerce_str_dict(raw.get("env"), "env")
|
||||
headers = _coerce_str_dict(raw.get("headers"), "headers")
|
||||
|
||||
transport_hint = raw.get("transport", raw.get("type"))
|
||||
|
||||
if command:
|
||||
transport: Transport = "stdio"
|
||||
elif url:
|
||||
try:
|
||||
transport = _normalize_transport(str(transport_hint) if transport_hint is not None else None)
|
||||
except ClaudeConfigError:
|
||||
transport = "streamable_http"
|
||||
else:
|
||||
raise ClaudeConfigError(f"server '{name}' must have either 'command' or 'url'")
|
||||
|
||||
servers.append(
|
||||
ClaudeMcpServer(
|
||||
name=name,
|
||||
transport=transport,
|
||||
command=command,
|
||||
args=args,
|
||||
env=env,
|
||||
url=url,
|
||||
headers=headers,
|
||||
enabled=enabled,
|
||||
)
|
||||
)
|
||||
|
||||
return servers
|
||||
|
||||
|
||||
def legacy_servers_list_to_claude_config(servers_list_json: str) -> str:
|
||||
"""Convert legacy v1.x servers list (JSON array) to Claude mcpServers JSON.
|
||||
|
||||
Legacy item schema:
|
||||
{"name","enabled","transport","url","headers","command","args","env"}
|
||||
"""
|
||||
text = (servers_list_json or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return ""
|
||||
if isinstance(data, dict):
|
||||
data = [data]
|
||||
if not isinstance(data, list):
|
||||
return ""
|
||||
|
||||
mcp_servers: Dict[str, Any] = {}
|
||||
for item in data:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = str(item.get("name", "") or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
enabled = bool(item.get("enabled", True))
|
||||
transport = str(item.get("transport", "") or "").strip().lower().replace("-", "_")
|
||||
|
||||
if transport == "stdio" or item.get("command"):
|
||||
entry: Dict[str, Any] = {
|
||||
"enabled": enabled,
|
||||
"command": item.get("command", "") or "",
|
||||
"args": item.get("args", []) or [],
|
||||
}
|
||||
if item.get("env"):
|
||||
entry["env"] = item.get("env")
|
||||
mcp_servers[name] = entry
|
||||
continue
|
||||
|
||||
entry = {"enabled": enabled, "url": item.get("url", "") or ""}
|
||||
if item.get("headers"):
|
||||
entry["headers"] = item.get("headers")
|
||||
if transport:
|
||||
entry["transport"] = transport
|
||||
mcp_servers[name] = entry
|
||||
|
||||
if not mcp_servers:
|
||||
return ""
|
||||
return json.dumps({"mcpServers": mcp_servers}, ensure_ascii=False, indent=2)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,278 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
MCP 客户端测试脚本
|
||||
测试 mcp_client.py 的基本功能
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 确保当前目录在 path 中
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from mcp_client import (
|
||||
MCPClientManager,
|
||||
MCPServerConfig,
|
||||
TransportType,
|
||||
ToolCallStats,
|
||||
ServerStats,
|
||||
)
|
||||
|
||||
|
||||
async def test_stats():
|
||||
"""测试统计类"""
|
||||
print("\n=== 测试统计类 ===")
|
||||
|
||||
# 测试 ToolCallStats
|
||||
stats = ToolCallStats(tool_key="test_tool")
|
||||
stats.record_call(True, 100.0)
|
||||
stats.record_call(True, 200.0)
|
||||
stats.record_call(False, 50.0, "timeout")
|
||||
|
||||
assert stats.total_calls == 3
|
||||
assert stats.success_calls == 2
|
||||
assert stats.failed_calls == 1
|
||||
assert stats.success_rate == (2 / 3) * 100
|
||||
assert stats.avg_duration_ms == 150.0
|
||||
assert stats.last_error == "timeout"
|
||||
|
||||
print(f"✅ ToolCallStats: {stats.to_dict()}")
|
||||
|
||||
# 测试 ServerStats
|
||||
server_stats = ServerStats(server_name="test_server")
|
||||
server_stats.record_connect()
|
||||
server_stats.record_heartbeat()
|
||||
server_stats.record_disconnect()
|
||||
server_stats.record_failure()
|
||||
server_stats.record_failure()
|
||||
|
||||
assert server_stats.connect_count == 1
|
||||
assert server_stats.disconnect_count == 1
|
||||
assert server_stats.consecutive_failures == 2
|
||||
|
||||
print(f"✅ ServerStats: {server_stats.to_dict()}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def test_manager_basic():
|
||||
"""测试管理器基本功能"""
|
||||
print("\n=== 测试管理器基本功能 ===")
|
||||
|
||||
# 创建新的管理器实例(绕过单例)
|
||||
manager = MCPClientManager.__new__(MCPClientManager)
|
||||
manager._initialized = False
|
||||
manager.__init__()
|
||||
|
||||
# 配置
|
||||
manager.configure(
|
||||
{
|
||||
"tool_prefix": "mcp",
|
||||
"call_timeout": 30.0,
|
||||
"retry_attempts": 1,
|
||||
"retry_interval": 1.0,
|
||||
"heartbeat_enabled": False,
|
||||
}
|
||||
)
|
||||
|
||||
# 测试状态
|
||||
status = manager.get_status()
|
||||
assert status["total_servers"] == 0
|
||||
assert status["connected_servers"] == 0
|
||||
print(f"✅ 初始状态: {status}")
|
||||
|
||||
# 测试添加禁用的服务器
|
||||
config = MCPServerConfig(
|
||||
name="disabled_server", enabled=False, transport=TransportType.HTTP, url="https://example.com/mcp"
|
||||
)
|
||||
result = await manager.add_server(config)
|
||||
assert result == True
|
||||
assert "disabled_server" in manager._clients
|
||||
assert manager._clients["disabled_server"].is_connected == False
|
||||
print("✅ 添加禁用服务器成功")
|
||||
|
||||
# 测试重复添加
|
||||
result = await manager.add_server(config)
|
||||
assert result == False
|
||||
print("✅ 重复添加被拒绝")
|
||||
|
||||
# 测试移除
|
||||
result = await manager.remove_server("disabled_server")
|
||||
assert result == True
|
||||
assert "disabled_server" not in manager._clients
|
||||
print("✅ 移除服务器成功")
|
||||
|
||||
# 清理
|
||||
await manager.shutdown()
|
||||
print("✅ 管理器关闭成功")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def test_http_connection():
|
||||
"""测试 HTTP 连接(使用真实的 MCP 服务器)"""
|
||||
print("\n=== 测试 HTTP 连接 ===")
|
||||
|
||||
# 创建新的管理器实例
|
||||
manager = MCPClientManager.__new__(MCPClientManager)
|
||||
manager._initialized = False
|
||||
manager.__init__()
|
||||
|
||||
manager.configure(
|
||||
{
|
||||
"tool_prefix": "mcp",
|
||||
"call_timeout": 30.0,
|
||||
"retry_attempts": 2,
|
||||
"retry_interval": 2.0,
|
||||
"heartbeat_enabled": False,
|
||||
}
|
||||
)
|
||||
|
||||
# 使用 HowToCook MCP 服务器测试
|
||||
config = MCPServerConfig(
|
||||
name="howtocook",
|
||||
enabled=True,
|
||||
transport=TransportType.HTTP,
|
||||
url="https://mcp.api-inference.modelscope.net/c9b55951d4ed47/mcp",
|
||||
)
|
||||
|
||||
print(f"正在连接 {config.url} ...")
|
||||
result = await manager.add_server(config)
|
||||
|
||||
if result:
|
||||
print("✅ 连接成功!")
|
||||
|
||||
# 检查工具
|
||||
tools = manager.all_tools
|
||||
print(f"✅ 发现 {len(tools)} 个工具:")
|
||||
for tool_key in tools:
|
||||
print(f" - {tool_key}")
|
||||
|
||||
# 测试心跳
|
||||
client = manager._clients["howtocook"]
|
||||
healthy = await client.check_health()
|
||||
print(f"✅ 心跳检测: {'健康' if healthy else '异常'}")
|
||||
|
||||
# 测试工具调用
|
||||
if "mcp_howtocook_whatToEat" in tools:
|
||||
print("\n正在调用 whatToEat 工具...")
|
||||
call_result = await manager.call_tool("mcp_howtocook_whatToEat", {})
|
||||
if call_result.success:
|
||||
print(f"✅ 工具调用成功 (耗时: {call_result.duration_ms:.0f}ms)")
|
||||
print(
|
||||
f" 结果: {call_result.content[:200]}..."
|
||||
if len(str(call_result.content)) > 200
|
||||
else f" 结果: {call_result.content}"
|
||||
)
|
||||
else:
|
||||
print(f"❌ 工具调用失败: {call_result.error}")
|
||||
|
||||
# 查看统计
|
||||
stats = manager.get_all_stats()
|
||||
print("\n📊 统计信息:")
|
||||
print(f" 全局调用: {stats['global']['total_tool_calls']}")
|
||||
print(f" 成功: {stats['global']['successful_calls']}")
|
||||
print(f" 失败: {stats['global']['failed_calls']}")
|
||||
|
||||
else:
|
||||
print("❌ 连接失败")
|
||||
|
||||
# 清理
|
||||
await manager.shutdown()
|
||||
return result
|
||||
|
||||
|
||||
async def test_heartbeat():
|
||||
"""测试心跳检测功能"""
|
||||
print("\n=== 测试心跳检测 ===")
|
||||
|
||||
# 创建新的管理器实例
|
||||
manager = MCPClientManager.__new__(MCPClientManager)
|
||||
manager._initialized = False
|
||||
manager.__init__()
|
||||
|
||||
manager.configure(
|
||||
{
|
||||
"tool_prefix": "mcp",
|
||||
"call_timeout": 30.0,
|
||||
"retry_attempts": 1,
|
||||
"retry_interval": 1.0,
|
||||
"heartbeat_enabled": True,
|
||||
"heartbeat_interval": 5.0, # 5秒间隔用于测试
|
||||
"auto_reconnect": True,
|
||||
"max_reconnect_attempts": 2,
|
||||
}
|
||||
)
|
||||
|
||||
# 添加一个测试服务器
|
||||
config = MCPServerConfig(
|
||||
name="heartbeat_test",
|
||||
enabled=True,
|
||||
transport=TransportType.HTTP,
|
||||
url="https://mcp.api-inference.modelscope.net/c9b55951d4ed47/mcp",
|
||||
)
|
||||
|
||||
print("正在连接服务器...")
|
||||
result = await manager.add_server(config)
|
||||
|
||||
if result:
|
||||
print("✅ 服务器连接成功")
|
||||
|
||||
# 启动心跳检测
|
||||
await manager.start_heartbeat()
|
||||
print("✅ 心跳检测已启动")
|
||||
|
||||
# 等待一个心跳周期
|
||||
print("等待心跳检测...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 检查状态
|
||||
status = manager.get_status()
|
||||
print(f"✅ 心跳运行状态: {status['heartbeat_running']}")
|
||||
|
||||
# 停止心跳
|
||||
await manager.stop_heartbeat()
|
||||
print("✅ 心跳检测已停止")
|
||||
else:
|
||||
print("❌ 服务器连接失败,跳过心跳测试")
|
||||
|
||||
await manager.shutdown()
|
||||
return True
|
||||
|
||||
|
||||
async def main():
|
||||
"""运行所有测试"""
|
||||
print("=" * 50)
|
||||
print("MCP 客户端测试")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
# 基础测试
|
||||
await test_stats()
|
||||
await test_manager_basic()
|
||||
|
||||
# 网络测试
|
||||
print("\n是否进行网络连接测试? (需要网络) [y/N]: ", end="")
|
||||
# 自动进行网络测试
|
||||
await test_http_connection()
|
||||
|
||||
# 心跳测试
|
||||
await test_heartbeat()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("✅ 所有测试通过!")
|
||||
print("=" * 50)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 测试失败: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -0,0 +1,582 @@
|
|||
"""
|
||||
MCP Workflow 模块 v1.9.0
|
||||
支持用户自定义工作流(硬流程),将多个 MCP 工具按顺序执行
|
||||
|
||||
双轨制架构:
|
||||
- 软流程 (ReAct): LLM 自主决策,动态多轮调用工具,灵活但不可预测
|
||||
- 硬流程 (Workflow): 用户预定义的工作流,固定流程,可靠可控
|
||||
|
||||
功能:
|
||||
- Workflow 定义和管理
|
||||
- 顺序执行多个工具(硬流程)
|
||||
- 支持变量替换(使用前序工具的输出)
|
||||
- 自动注册为组合工具供 LLM 调用
|
||||
- 与 ReAct 软流程互补,用户可选择合适的执行方式
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||
|
||||
try:
|
||||
from src.common.logger import get_logger
|
||||
logger = get_logger("mcp_tool_chain")
|
||||
except ImportError:
|
||||
import logging
|
||||
logger = logging.getLogger("mcp_tool_chain")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolChainStep:
|
||||
"""工具链步骤"""
|
||||
tool_name: str # 要调用的工具名(如 mcp_server_tool)
|
||||
args_template: Dict[str, Any] = field(default_factory=dict) # 参数模板,支持变量替换
|
||||
output_key: str = "" # 输出存储的键名,供后续步骤引用
|
||||
description: str = "" # 步骤描述
|
||||
optional: bool = False # 是否可选(失败时继续执行)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"tool_name": self.tool_name,
|
||||
"args_template": self.args_template,
|
||||
"output_key": self.output_key,
|
||||
"description": self.description,
|
||||
"optional": self.optional,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ToolChainStep":
|
||||
return cls(
|
||||
tool_name=data.get("tool_name", ""),
|
||||
args_template=data.get("args_template", {}),
|
||||
output_key=data.get("output_key", ""),
|
||||
description=data.get("description", ""),
|
||||
optional=data.get("optional", False),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolChainDefinition:
|
||||
"""工具链定义"""
|
||||
name: str # 工具链名称(将作为组合工具的名称)
|
||||
description: str # 工具链描述(供 LLM 理解)
|
||||
steps: List[ToolChainStep] = field(default_factory=list) # 执行步骤
|
||||
input_params: Dict[str, str] = field(default_factory=dict) # 输入参数定义 {参数名: 描述}
|
||||
enabled: bool = True # 是否启用
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"steps": [step.to_dict() for step in self.steps],
|
||||
"input_params": self.input_params,
|
||||
"enabled": self.enabled,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ToolChainDefinition":
|
||||
steps = [ToolChainStep.from_dict(s) for s in data.get("steps", [])]
|
||||
return cls(
|
||||
name=data.get("name", ""),
|
||||
description=data.get("description", ""),
|
||||
steps=steps,
|
||||
input_params=data.get("input_params", {}),
|
||||
enabled=data.get("enabled", True),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChainExecutionResult:
|
||||
"""工具链执行结果"""
|
||||
success: bool
|
||||
final_output: str # 最终输出(最后一个步骤的结果)
|
||||
step_results: List[Dict[str, Any]] = field(default_factory=list) # 每个步骤的结果
|
||||
error: str = ""
|
||||
total_duration_ms: float = 0.0
|
||||
|
||||
def to_summary(self) -> str:
|
||||
"""生成执行摘要"""
|
||||
lines = []
|
||||
for i, step in enumerate(self.step_results):
|
||||
status = "✅" if step.get("success") else "❌"
|
||||
tool = step.get("tool_name", "unknown")
|
||||
duration = step.get("duration_ms", 0)
|
||||
lines.append(f"{status} 步骤{i+1}: {tool} ({duration:.0f}ms)")
|
||||
if not step.get("success") and step.get("error"):
|
||||
lines.append(f" 错误: {step['error'][:50]}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class ToolChainExecutor:
|
||||
"""工具链执行器"""
|
||||
|
||||
# 变量替换模式: ${step.output_key} 或 ${input.param_name} 或 ${prev}
|
||||
VAR_PATTERN = re.compile(r'\$\{([^}]+)\}')
|
||||
|
||||
def __init__(self, mcp_manager):
|
||||
self._mcp_manager = mcp_manager
|
||||
|
||||
def _resolve_tool_key(self, tool_name: str) -> Optional[str]:
|
||||
"""解析工具名,返回有效的 tool_key
|
||||
|
||||
支持:
|
||||
- 直接使用 tool_key(如 mcp_server_tool)
|
||||
- 使用注册后的工具名(会自动转换 - 和 . 为 _)
|
||||
"""
|
||||
all_tools = self._mcp_manager.all_tools
|
||||
|
||||
# 直接匹配
|
||||
if tool_name in all_tools:
|
||||
return tool_name
|
||||
|
||||
# 尝试转换后匹配(用户可能使用了注册后的名称)
|
||||
normalized = tool_name.replace("-", "_").replace(".", "_")
|
||||
if normalized in all_tools:
|
||||
return normalized
|
||||
|
||||
# 尝试查找包含该名称的工具
|
||||
for key in all_tools.keys():
|
||||
if key.endswith(f"_{tool_name}") or key.endswith(f"_{normalized}"):
|
||||
return key
|
||||
|
||||
return None
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
chain: ToolChainDefinition,
|
||||
input_args: Dict[str, Any],
|
||||
) -> ChainExecutionResult:
|
||||
"""执行工具链
|
||||
|
||||
Args:
|
||||
chain: 工具链定义
|
||||
input_args: 用户输入的参数
|
||||
|
||||
Returns:
|
||||
ChainExecutionResult: 执行结果
|
||||
"""
|
||||
start_time = time.time()
|
||||
step_results = []
|
||||
context = {
|
||||
"input": input_args or {}, # 用户输入,确保不为 None
|
||||
"step": {}, # 各步骤输出,按 output_key 存储
|
||||
"prev": "", # 上一步的输出
|
||||
}
|
||||
|
||||
final_output = ""
|
||||
|
||||
# 验证必需的输入参数
|
||||
missing_params = []
|
||||
for param_name in chain.input_params.keys():
|
||||
if param_name not in context["input"]:
|
||||
missing_params.append(param_name)
|
||||
|
||||
if missing_params:
|
||||
return ChainExecutionResult(
|
||||
success=False,
|
||||
final_output="",
|
||||
error=f"缺少必需参数: {', '.join(missing_params)}",
|
||||
total_duration_ms=(time.time() - start_time) * 1000,
|
||||
)
|
||||
|
||||
for i, step in enumerate(chain.steps):
|
||||
step_start = time.time()
|
||||
step_result = {
|
||||
"step_index": i,
|
||||
"tool_name": step.tool_name,
|
||||
"success": False,
|
||||
"output": "",
|
||||
"error": "",
|
||||
"duration_ms": 0,
|
||||
}
|
||||
|
||||
try:
|
||||
# 替换参数中的变量
|
||||
resolved_args = self._resolve_args(step.args_template, context)
|
||||
step_result["resolved_args"] = resolved_args
|
||||
|
||||
# 解析工具名
|
||||
tool_key = self._resolve_tool_key(step.tool_name)
|
||||
if not tool_key:
|
||||
step_result["error"] = f"工具 {step.tool_name} 不存在"
|
||||
logger.warning(f"工具链步骤 {i+1}: 工具 {step.tool_name} 不存在")
|
||||
|
||||
if not step.optional:
|
||||
step_results.append(step_result)
|
||||
return ChainExecutionResult(
|
||||
success=False,
|
||||
final_output="",
|
||||
step_results=step_results,
|
||||
error=f"步骤 {i+1}: 工具 {step.tool_name} 不存在",
|
||||
total_duration_ms=(time.time() - start_time) * 1000,
|
||||
)
|
||||
step_results.append(step_result)
|
||||
continue
|
||||
|
||||
logger.debug(f"工具链步骤 {i+1}: 调用 {tool_key},参数: {resolved_args}")
|
||||
|
||||
# 调用工具
|
||||
result = await self._mcp_manager.call_tool(tool_key, resolved_args)
|
||||
|
||||
step_duration = (time.time() - step_start) * 1000
|
||||
step_result["duration_ms"] = step_duration
|
||||
|
||||
if result.success:
|
||||
step_result["success"] = True
|
||||
# 确保 content 不为 None
|
||||
content = result.content if result.content is not None else ""
|
||||
step_result["output"] = content
|
||||
|
||||
# 更新上下文
|
||||
context["prev"] = content
|
||||
if step.output_key:
|
||||
context["step"][step.output_key] = content
|
||||
|
||||
final_output = content
|
||||
content_preview = content[:100] if content else "(空)"
|
||||
logger.debug(f"工具链步骤 {i+1} 成功: {content_preview}...")
|
||||
else:
|
||||
step_result["error"] = result.error or "未知错误"
|
||||
logger.warning(f"工具链步骤 {i+1} 失败: {result.error}")
|
||||
|
||||
if not step.optional:
|
||||
step_results.append(step_result)
|
||||
return ChainExecutionResult(
|
||||
success=False,
|
||||
final_output="",
|
||||
step_results=step_results,
|
||||
error=f"步骤 {i+1} ({step.tool_name}) 失败: {result.error}",
|
||||
total_duration_ms=(time.time() - start_time) * 1000,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
step_duration = (time.time() - step_start) * 1000
|
||||
step_result["duration_ms"] = step_duration
|
||||
step_result["error"] = str(e)
|
||||
logger.error(f"工具链步骤 {i+1} 异常: {e}")
|
||||
|
||||
if not step.optional:
|
||||
step_results.append(step_result)
|
||||
return ChainExecutionResult(
|
||||
success=False,
|
||||
final_output="",
|
||||
step_results=step_results,
|
||||
error=f"步骤 {i+1} ({step.tool_name}) 异常: {e}",
|
||||
total_duration_ms=(time.time() - start_time) * 1000,
|
||||
)
|
||||
|
||||
step_results.append(step_result)
|
||||
|
||||
total_duration = (time.time() - start_time) * 1000
|
||||
|
||||
return ChainExecutionResult(
|
||||
success=True,
|
||||
final_output=final_output,
|
||||
step_results=step_results,
|
||||
total_duration_ms=total_duration,
|
||||
)
|
||||
|
||||
def _resolve_args(self, args_template: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""解析参数模板,替换变量
|
||||
|
||||
支持的变量格式:
|
||||
- ${input.param_name}: 用户输入的参数
|
||||
- ${step.output_key}: 某个步骤的输出
|
||||
- ${prev}: 上一步的输出
|
||||
- ${prev.field}: 上一步输出(JSON)的某个字段
|
||||
"""
|
||||
resolved = {}
|
||||
|
||||
for key, value in args_template.items():
|
||||
if isinstance(value, str):
|
||||
resolved[key] = self._substitute_vars(value, context)
|
||||
elif isinstance(value, dict):
|
||||
resolved[key] = self._resolve_args(value, context)
|
||||
elif isinstance(value, list):
|
||||
resolved[key] = [
|
||||
self._substitute_vars(v, context) if isinstance(v, str) else v
|
||||
for v in value
|
||||
]
|
||||
else:
|
||||
resolved[key] = value
|
||||
|
||||
return resolved
|
||||
|
||||
def _substitute_vars(self, template: str, context: Dict[str, Any]) -> str:
|
||||
"""替换字符串中的变量"""
|
||||
def replacer(match):
|
||||
var_path = match.group(1)
|
||||
return self._get_var_value(var_path, context)
|
||||
|
||||
return self.VAR_PATTERN.sub(replacer, template)
|
||||
|
||||
def _get_var_value(self, var_path: str, context: Dict[str, Any]) -> str:
|
||||
"""获取变量值
|
||||
|
||||
Args:
|
||||
var_path: 变量路径,如 "input.query", "step.search_result", "prev", "prev.id"
|
||||
context: 上下文
|
||||
"""
|
||||
parts = self._parse_var_path(var_path)
|
||||
|
||||
if not parts:
|
||||
return ""
|
||||
|
||||
# 获取根对象
|
||||
root = parts[0]
|
||||
if root not in context:
|
||||
logger.warning(f"变量 {var_path} 的根 '{root}' 不存在")
|
||||
return ""
|
||||
|
||||
value = context[root]
|
||||
|
||||
# 遍历路径
|
||||
for part in parts[1:]:
|
||||
if isinstance(value, str):
|
||||
parsed = self._try_parse_json(value)
|
||||
if parsed is not None:
|
||||
value = parsed
|
||||
|
||||
if isinstance(value, dict):
|
||||
value = value.get(part, "")
|
||||
elif isinstance(value, list):
|
||||
if part.isdigit():
|
||||
idx = int(part)
|
||||
value = value[idx] if 0 <= idx < len(value) else ""
|
||||
else:
|
||||
value = ""
|
||||
else:
|
||||
value = ""
|
||||
|
||||
# 确保返回字符串
|
||||
if isinstance(value, (dict, list)):
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
if value is None:
|
||||
return ""
|
||||
if value == "":
|
||||
return ""
|
||||
return str(value)
|
||||
|
||||
def _try_parse_json(self, value: str) -> Optional[Any]:
|
||||
"""尝试将字符串解析为 JSON 对象,失败则返回 None。"""
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
return json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
def _parse_var_path(self, var_path: str) -> List[str]:
|
||||
"""解析变量路径,支持点号与下标写法。
|
||||
|
||||
支持:
|
||||
- step.geo.return.0.location
|
||||
- step.geo.return[0].location
|
||||
- step.geo['return'][0]['location']
|
||||
"""
|
||||
if not var_path:
|
||||
return []
|
||||
|
||||
tokens: List[str] = []
|
||||
buf: List[str] = []
|
||||
in_bracket = False
|
||||
in_quote = False
|
||||
quote_char = ""
|
||||
|
||||
def flush_buf() -> None:
|
||||
if buf:
|
||||
token = "".join(buf).strip()
|
||||
if token:
|
||||
tokens.append(token)
|
||||
buf.clear()
|
||||
|
||||
i = 0
|
||||
while i < len(var_path):
|
||||
ch = var_path[i]
|
||||
|
||||
if not in_bracket and ch == ".":
|
||||
flush_buf()
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if not in_bracket and ch == "[":
|
||||
flush_buf()
|
||||
in_bracket = True
|
||||
in_quote = False
|
||||
quote_char = ""
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if in_bracket and not in_quote and ch == "]":
|
||||
flush_buf()
|
||||
in_bracket = False
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if in_bracket and ch in ("'", '"'):
|
||||
if not in_quote:
|
||||
in_quote = True
|
||||
quote_char = ch
|
||||
i += 1
|
||||
continue
|
||||
if quote_char == ch:
|
||||
in_quote = False
|
||||
quote_char = ""
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if in_bracket and not in_quote:
|
||||
if ch.isspace():
|
||||
i += 1
|
||||
continue
|
||||
if ch == ",":
|
||||
i += 1
|
||||
continue
|
||||
|
||||
buf.append(ch)
|
||||
i += 1
|
||||
|
||||
flush_buf()
|
||||
|
||||
if in_bracket or in_quote:
|
||||
return [p for p in var_path.split(".") if p]
|
||||
|
||||
return tokens
|
||||
|
||||
|
||||
class ToolChainManager:
|
||||
"""工具链管理器"""
|
||||
|
||||
_instance: Optional["ToolChainManager"] = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if self._initialized:
|
||||
return
|
||||
self._initialized = True
|
||||
self._chains: Dict[str, ToolChainDefinition] = {}
|
||||
self._executor: Optional[ToolChainExecutor] = None
|
||||
|
||||
def set_executor(self, mcp_manager) -> None:
|
||||
"""设置执行器"""
|
||||
self._executor = ToolChainExecutor(mcp_manager)
|
||||
|
||||
def add_chain(self, chain: ToolChainDefinition) -> bool:
|
||||
"""添加工具链"""
|
||||
if not chain.name:
|
||||
logger.error("工具链名称不能为空")
|
||||
return False
|
||||
|
||||
if chain.name in self._chains:
|
||||
logger.warning(f"工具链 {chain.name} 已存在,将被覆盖")
|
||||
|
||||
self._chains[chain.name] = chain
|
||||
logger.info(f"已添加工具链: {chain.name} ({len(chain.steps)} 个步骤)")
|
||||
return True
|
||||
|
||||
def remove_chain(self, name: str) -> bool:
|
||||
"""移除工具链"""
|
||||
if name in self._chains:
|
||||
del self._chains[name]
|
||||
logger.info(f"已移除工具链: {name}")
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_chain(self, name: str) -> Optional[ToolChainDefinition]:
|
||||
"""获取工具链"""
|
||||
return self._chains.get(name)
|
||||
|
||||
def get_all_chains(self) -> Dict[str, ToolChainDefinition]:
|
||||
"""获取所有工具链"""
|
||||
return self._chains.copy()
|
||||
|
||||
def get_enabled_chains(self) -> Dict[str, ToolChainDefinition]:
|
||||
"""获取所有启用的工具链"""
|
||||
return {name: chain for name, chain in self._chains.items() if chain.enabled}
|
||||
|
||||
async def execute_chain(
|
||||
self,
|
||||
chain_name: str,
|
||||
input_args: Dict[str, Any],
|
||||
) -> ChainExecutionResult:
|
||||
"""执行工具链"""
|
||||
chain = self._chains.get(chain_name)
|
||||
if not chain:
|
||||
return ChainExecutionResult(
|
||||
success=False,
|
||||
final_output="",
|
||||
error=f"工具链 {chain_name} 不存在",
|
||||
)
|
||||
|
||||
if not chain.enabled:
|
||||
return ChainExecutionResult(
|
||||
success=False,
|
||||
final_output="",
|
||||
error=f"工具链 {chain_name} 已禁用",
|
||||
)
|
||||
|
||||
if not self._executor:
|
||||
return ChainExecutionResult(
|
||||
success=False,
|
||||
final_output="",
|
||||
error="工具链执行器未初始化",
|
||||
)
|
||||
|
||||
return await self._executor.execute(chain, input_args)
|
||||
|
||||
def load_from_json(self, json_str: str) -> Tuple[int, List[str]]:
|
||||
"""从 JSON 字符串加载工具链配置
|
||||
|
||||
Returns:
|
||||
(成功加载数量, 错误列表)
|
||||
"""
|
||||
errors = []
|
||||
loaded = 0
|
||||
|
||||
try:
|
||||
data = json.loads(json_str) if json_str.strip() else []
|
||||
except json.JSONDecodeError as e:
|
||||
return 0, [f"JSON 解析失败: {e}"]
|
||||
|
||||
if not isinstance(data, list):
|
||||
data = [data]
|
||||
|
||||
for i, item in enumerate(data):
|
||||
try:
|
||||
chain = ToolChainDefinition.from_dict(item)
|
||||
if not chain.name:
|
||||
errors.append(f"第 {i+1} 个工具链缺少名称")
|
||||
continue
|
||||
if not chain.steps:
|
||||
errors.append(f"工具链 {chain.name} 没有步骤")
|
||||
continue
|
||||
|
||||
self.add_chain(chain)
|
||||
loaded += 1
|
||||
except Exception as e:
|
||||
errors.append(f"第 {i+1} 个工具链解析失败: {e}")
|
||||
|
||||
return loaded, errors
|
||||
|
||||
def export_to_json(self, pretty: bool = True) -> str:
|
||||
"""导出所有工具链为 JSON"""
|
||||
chains_data = [chain.to_dict() for chain in self._chains.values()]
|
||||
if pretty:
|
||||
return json.dumps(chains_data, ensure_ascii=False, indent=2)
|
||||
return json.dumps(chains_data, ensure_ascii=False)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""清空所有工具链"""
|
||||
self._chains.clear()
|
||||
|
||||
|
||||
# 全局工具链管理器实例
|
||||
tool_chain_manager = ToolChainManager()
|
||||
Loading…
Reference in New Issue