File size: 13,630 Bytes
6f89625 10b8d56 6f89625 d0c18f0 6f89625 10b8d56 6f89625 10b8d56 6f89625 10b8d56 6f89625 10b8d56 d0c18f0 6f89625 10b8d56 6f89625 10b8d56 6f89625 d0c18f0 6f89625 d0c18f0 6f89625 cc826a1 e5e756a 6f89625 10b8d56 6f89625 10b8d56 6f89625 10b8d56 6f89625 10b8d56 6f89625 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 | """
插件MCP工具动态注册器 - 扫描插件并注册MCP工具
扩展功能:
- 工具可用性绑定插件生命周期
- 最近错误记录
- 元数据完整性检查
"""
import importlib.util
import sys
import logging
import functools
import inspect
from pathlib import Path
from typing import Dict, Any, List, TYPE_CHECKING, Callable, Optional
from datetime import datetime
if TYPE_CHECKING:
from mcp.server import FastMCP
logger = logging.getLogger(__name__)
class MCPToolAvailability:
"""MCP 工具可用状态常量"""
AVAILABLE = "available"
UNAVAILABLE = "unavailable"
INCOMPLETE = "incomplete"
class PluginMCPRegistry:
"""插件MCP工具动态注册器
扩展功能:
- 工具可用性绑定插件生命周期
- 最近错误记录
- 元数据完整性检查
"""
def __init__(self, mcp_server: "FastMCP"):
self.mcp = mcp_server
self.registered_tools: Dict[str, Dict[str, Any]] = {}
self._tool_wrappers: Dict[str, Callable] = {}
def _create_tool_wrapper(self, tool_name: str, original_func: Callable) -> Callable:
"""创建工具调用包装器,保持原始函数签名用于 schema 生成。
包装器在调用前查询插件状态和依赖状态,不可用时直接返回结构化错误。
捕获参数错误和运行异常,写入最近错误记录。
"""
@functools.wraps(original_func)
async def wrapper(*args, **kwargs):
# 检查工具是否在注册表中
tool_info = self.registered_tools.get(tool_name)
if not tool_info:
return {
"success": False,
"error": f"工具 {tool_name} 不存在",
"error_code": "TOOL_NOT_FOUND",
}
# 检查工具可用性
availability = tool_info.get("availability", MCPToolAvailability.AVAILABLE)
if availability == MCPToolAvailability.UNAVAILABLE:
reason = tool_info.get("unavailable_reason", "工具不可用")
return {
"success": False,
"error": f"工具 {tool_name} 不可用:{reason}",
"error_code": "TOOL_UNAVAILABLE",
"unavailable_reason": reason,
}
# 记录调用时间
tool_info["last_called_at"] = datetime.now()
try:
# 调用原始函数
result = await original_func(*args, **kwargs)
return result
except TypeError as e:
# 参数错误
error_msg = f"参数错误:{str(e)}"
tool_info["last_error"] = {
"error_code": "INVALID_PARAMETERS",
"message": error_msg,
"occurred_at": datetime.now().isoformat(),
}
logger.error(f"工具 {tool_name} 参数错误: {e}")
return {
"success": False,
"error": error_msg,
"error_code": "INVALID_PARAMETERS",
}
except Exception as e:
# 运行时错误
error_msg = f"运行错误:{str(e)}"
tool_info["last_error"] = {
"error_code": "RUNTIME_ERROR",
"message": error_msg,
"occurred_at": datetime.now().isoformat(),
}
logger.error(f"工具 {tool_name} 运行错误: {e}")
return {
"success": False,
"error": error_msg,
"error_code": "RUNTIME_ERROR",
}
# 保持原始函数签名
wrapper.__signature__ = inspect.signature(original_func)
return wrapper
def scan_and_register_all(self, plugins_dir: Path) -> int:
"""扫描所有插件并注册MCP工具。"""
total = 0
for plugin_dir in plugins_dir.iterdir():
if plugin_dir.is_dir() and not plugin_dir.name.startswith('_'):
count = self.register_plugin_tools(plugin_dir)
total += count
if count > 0:
logger.info(f"插件 {plugin_dir.name} 注册了 {count} 个MCP工具")
return total
def register_plugin_tools_by_name(self, plugins_dir: Path, plugin_names: List[str]) -> int:
"""仅注册指定插件的 MCP 工具"""
total = 0
for plugin_name in plugin_names:
plugin_dir = plugins_dir / plugin_name
if plugin_dir.is_dir() and not plugin_dir.name.startswith('_'):
count = self.register_plugin_tools(plugin_dir)
total += count
if count > 0:
logger.info(f"插件 {plugin_name} 注册了 {count} 个MCP工具")
return total
def register_plugin_tools(self, plugin_dir: Path) -> int:
"""注册单个插件的MCP工具。"""
mcp_file = plugin_dir / "mcp.py"
if not mcp_file.exists():
return 0
plugin_name = plugin_dir.name
module = self._import_module(mcp_file, plugin_name)
if not module:
return 0
tools = self._scan_tools(module)
count = 0
for tool_def in tools:
# 直接使用 decorator 中定义的名称(约定:{插件名}-{工具名})
full_name = tool_def['name']
try:
wrapped_func = self._create_tool_wrapper(full_name, tool_def['func'])
self._tool_wrappers[full_name] = wrapped_func
self.mcp.tool(
name=full_name,
title=tool_def.get('title'),
description=tool_def.get('description'),
)(wrapped_func)
# 检查元数据完整性
incomplete_reasons = []
if not tool_def.get('title'):
incomplete_reasons.append("缺少标题")
if not tool_def.get('description'):
incomplete_reasons.append("缺少描述")
availability = MCPToolAvailability.AVAILABLE
if incomplete_reasons:
availability = MCPToolAvailability.INCOMPLETE
self.registered_tools[full_name] = {
"plugin": plugin_name,
"tool": tool_def['name'],
"input_model": tool_def.get('input_model'),
"output_model": tool_def.get('output_model'),
"title": tool_def.get('title'),
"description": tool_def.get('description'),
# 新增字段
"availability": availability,
"unavailable_reason": None,
"incomplete_reasons": incomplete_reasons if incomplete_reasons else None,
"last_error": None,
"last_called_at": None,
"registered_at": datetime.now().isoformat(),
}
count += 1
except Exception as e:
logger.error(f"注册工具 {full_name} 失败: {e}")
return count
def _import_module(self, mcp_file: Path, plugin_name: str):
"""动态导入模块。"""
try:
spec = importlib.util.spec_from_file_location(
f"plugins.{plugin_name}.mcp", mcp_file
)
if not spec or not spec.loader:
return None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
except Exception as e:
logger.error(f"导入模块 {mcp_file} 失败: {e}")
return None
def _scan_tools(self, module) -> List[Dict]:
"""扫描模块中的工具定义。"""
tools = []
for attr_name in dir(module):
attr = getattr(module, attr_name)
if callable(attr) and hasattr(attr, '_mcp_tool_def'):
tools.append(attr._mcp_tool_def)
return tools
def get_tool_docs(self, plugin_name: str = None) -> Dict:
"""获取工具文档。"""
if plugin_name:
return {
name: info for name, info in self.registered_tools.items()
if info['plugin'] == plugin_name
}
return self.registered_tools.copy()
def unregister_plugin_tools(self, plugin_name: str) -> int:
"""注销插件的MCP工具。"""
tools_to_remove = [
name for name, info in self.registered_tools.items()
if info['plugin'] == plugin_name
]
for tool_name in tools_to_remove:
del self.registered_tools[tool_name]
logger.info(f"已注销插件 {plugin_name} 的 {len(tools_to_remove)} 个工具")
return len(tools_to_remove)
def get_plugin_tools(self, plugin_name: str) -> List[str]:
"""获取插件的所有工具名称。"""
return [
name for name, info in self.registered_tools.items()
if info['plugin'] == plugin_name
]
def set_tool_availability(
self,
tool_name: str,
availability: str,
unavailable_reason: Optional[str] = None,
) -> bool:
"""设置工具可用状态
Args:
tool_name: 工具全名
availability: 可用状态
unavailable_reason: 不可用原因
Returns:
是否设置成功
"""
tool_info = self.registered_tools.get(tool_name)
if not tool_info:
return False
tool_info["availability"] = availability
tool_info["unavailable_reason"] = unavailable_reason
return True
def set_plugin_tools_availability(
self,
plugin_name: str,
availability: str,
unavailable_reason: Optional[str] = None,
) -> int:
"""设置插件所有工具的可用状态
Args:
plugin_name: 插件名
availability: 可用状态
unavailable_reason: 不可用原因
Returns:
更新的工具数量
"""
count = 0
for tool_name, tool_info in self.registered_tools.items():
if tool_info["plugin"] == plugin_name:
tool_info["availability"] = availability
tool_info["unavailable_reason"] = unavailable_reason
count += 1
return count
def get_mcp_status(self) -> Dict[str, Any]:
"""获取 MCP 服务状态
Returns:
包含服务状态、工具总数、按插件分组、最近错误的字典
"""
total_tools = len(self.registered_tools)
available_tools = sum(
1 for info in self.registered_tools.values()
if info.get("availability") == MCPToolAvailability.AVAILABLE
)
unavailable_tools = sum(
1 for info in self.registered_tools.values()
if info.get("availability") == MCPToolAvailability.UNAVAILABLE
)
incomplete_tools = sum(
1 for info in self.registered_tools.values()
if info.get("availability") == MCPToolAvailability.INCOMPLETE
)
# 按插件分组
groups = {}
for tool_name, tool_info in self.registered_tools.items():
plugin_name = tool_info["plugin"]
if plugin_name not in groups:
groups[plugin_name] = {
"plugin_name": plugin_name,
"tools": [],
}
groups[plugin_name]["tools"].append({
"name": tool_name,
"title": tool_info.get("title"),
"description": tool_info.get("description"),
"availability": tool_info.get("availability"),
"unavailable_reason": tool_info.get("unavailable_reason"),
"incomplete_reasons": tool_info.get("incomplete_reasons"),
"last_error": tool_info.get("last_error"),
"last_called_at": tool_info.get("last_called_at"),
})
# 收集最近错误
recent_errors = []
for tool_name, tool_info in self.registered_tools.items():
if tool_info.get("last_error"):
recent_errors.append({
"tool_name": tool_name,
"plugin_name": tool_info["plugin"],
**tool_info["last_error"],
})
return {
"service_available": True,
"total_tools": total_tools,
"available_tools": available_tools,
"unavailable_tools": unavailable_tools,
"incomplete_tools": incomplete_tools,
"groups": list(groups.values()),
"recent_errors": recent_errors,
}
# 全局单例
_plugin_registry: Optional[PluginMCPRegistry] = None
def get_plugin_registry() -> PluginMCPRegistry:
"""获取全局插件注册器实例。"""
global _plugin_registry
if _plugin_registry is None:
raise RuntimeError("PluginMCPRegistry 未初始化,请先调用 init_plugin_registry(mcp)")
return _plugin_registry
def init_plugin_registry(mcp_server: "FastMCP") -> PluginMCPRegistry:
"""初始化全局插件注册器。"""
global _plugin_registry
_plugin_registry = PluginMCPRegistry(mcp_server)
return _plugin_registry |