mirror of https://github.com/Mai-with-u/MaiBot.git
171 lines
5.2 KiB
Python
171 lines
5.2 KiB
Python
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)
|
|
|