re:以直接代码而不是submodule形式添加内置插件

This commit is contained in:
SengokuCola
2025-12-06 00:50:23 +08:00
parent 8d7d7f0fb2
commit 1aaf129d46
12 changed files with 6556 additions and 31 deletions

View File

@@ -0,0 +1,448 @@
"""
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)