""" 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)