feat: MCP 客户端(连接外部 MCP server,外部工具桥接进统一工具体系)
- 新增 mcp_client.py:McpClient(subprocess stdio JSON-RPC,reader 线程+响应表, initialize/list_tools/call_tool/shutdown 超时可控)+ McpClientManager(connect_all 读 mcpServers、聚合、call_tool、cleanup 幂等) - unified_tool_manager.py:initialize 末尾加载外部 MCP server,按 server__tool 前缀 register_tool 带真实 inputSchema mcp_definition,shutdown 经 manager.cleanup 幂等终止子进程 - fc_tools.py:mcp_to_openai_tools 新增 get_mcp_tools 保真分支(同时修现状 liugin 工具 schema 被压成 args 的坑) - config.json:顶层加 mcpServers 空段(用户自填,已被 gitignore 不入库,代码 .get 兼容) - 新增 tests/test_mcp_client.py(5 项,全链路+桥接+保真,不触真实引擎/网络) Co-Authored-By: WorkBuddy <workbuddy@tencent.com>
This commit is contained in:
1 parent
a3977ecc85
commit
d2ad1000b6
4 files changed
+606
-2
No files matched your search
@@ -12,6 +12,11 @@ from colorama import Fore, Style
|
||||
from xcli_core.tool_result import ToolResult, ErrorCode
|
||||
from xcli_core.verbose import vprint
|
||||
|
||||
try:
|
||||
from mcp_client import McpClientManager
|
||||
except Exception:
|
||||
McpClientManager = None
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -48,6 +53,9 @@ class UnifiedToolManager:
|
||||
# MCP 工具定义缓存
|
||||
self._mcp_tools_cache: Optional[List[Dict]] = None
|
||||
|
||||
# 外部 MCP client 管理器(懒加载)
|
||||
self._mcp_client_manager = None
|
||||
|
||||
def initialize(self, plugins_dir: str = "plugins", skills_dir: str = "skills") -> None:
|
||||
"""初始化并加载所有工具"""
|
||||
self._plugins_dir = plugins_dir
|
||||
@@ -55,6 +63,7 @@ class UnifiedToolManager:
|
||||
self._load_plugins(plugins_dir)
|
||||
self._load_skills(skills_dir)
|
||||
self._build_mcp_cache()
|
||||
self._load_mcp_servers()
|
||||
|
||||
n_liugin = sum(1 for p in self._protocol_map.values() if p == 'liugin')
|
||||
n_skill = sum(1 for p in self._protocol_map.values() if p == 'skill')
|
||||
@@ -302,6 +311,93 @@ class UnifiedToolManager:
|
||||
}
|
||||
}
|
||||
|
||||
# ==================== 外部 MCP 客户端桥接 ====================
|
||||
|
||||
def _load_mcp_servers(self) -> None:
|
||||
"""连接 config.json 中声明的外部 MCP server,将其工具桥接进统一注册表。
|
||||
|
||||
外部工具以 ``{server}__{tool}`` 命名注册,schema(inputSchema)保真,
|
||||
模型可像调用本地工具一样调用外部 MCP 工具。
|
||||
"""
|
||||
if McpClientManager is None:
|
||||
return
|
||||
try:
|
||||
from config import load_config
|
||||
except ImportError:
|
||||
try:
|
||||
from xcli_core.config import load_config
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
try:
|
||||
cfg = load_config()
|
||||
servers = cfg.get("mcpServers") or {}
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if not servers:
|
||||
return
|
||||
|
||||
if self._mcp_client_manager is None:
|
||||
self._mcp_client_manager = McpClientManager()
|
||||
|
||||
# unified_tool_manager.py 位于项目根,故 dirname(__file__) 即项目根
|
||||
project_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
try:
|
||||
self._mcp_client_manager.connect_all(servers, cwd=project_dir)
|
||||
except Exception as e:
|
||||
print(f"{Fore.YELLOW}加载外部 MCP server 失败: {e}{Style.RESET_ALL}")
|
||||
return
|
||||
|
||||
added = 0
|
||||
for server_name, tool_def in self._mcp_client_manager.get_all_tools():
|
||||
remote_name = tool_def.get("name")
|
||||
if not remote_name:
|
||||
continue
|
||||
local_name = f"{server_name}__{remote_name}"
|
||||
mcp_def = {
|
||||
"name": local_name,
|
||||
"description": tool_def.get("description", ""),
|
||||
"inputSchema": tool_def.get("inputSchema", {"type": "object", "properties": {}}),
|
||||
}
|
||||
handler = self._make_external_handler(server_name, remote_name)
|
||||
# instance 存 manager,shutdown 时统一 cleanup(McpClientManager.cleanup 幂等)
|
||||
self.register_tool(
|
||||
local_name, handler,
|
||||
description=mcp_def["description"],
|
||||
protocol="mcp",
|
||||
mcp_definition=mcp_def,
|
||||
instance=self._mcp_client_manager,
|
||||
)
|
||||
added += 1
|
||||
|
||||
if added:
|
||||
n_servers = len(self._mcp_client_manager.get_servers())
|
||||
print(f"{Fore.GREEN}已桥接 {added} 个外部 MCP 工具{Style.RESET_ALL}"
|
||||
f"{Fore.BLACK}{Style.BRIGHT} (来自 {n_servers} 个 server){Style.RESET_ALL}")
|
||||
|
||||
def _make_external_handler(self, server_name: str, remote_name: str):
|
||||
"""为某个外部 MCP 工具生成 handler:解析参数 → call_tool → ToolResult"""
|
||||
def handler(args):
|
||||
try:
|
||||
if isinstance(args, dict):
|
||||
arguments = args
|
||||
elif isinstance(args, str) and args.strip().startswith("{"):
|
||||
arguments = json.loads(args)
|
||||
else:
|
||||
arguments = {"args": args}
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
arguments = {"args": args}
|
||||
|
||||
try:
|
||||
result_text = self._mcp_client_manager.call_tool(server_name, remote_name, arguments)
|
||||
return ToolResult.ok(result_text, tool_name=f"{server_name}__{remote_name}")
|
||||
except Exception as e:
|
||||
return ToolResult.fail(str(e), ErrorCode.EXEC_FAILED,
|
||||
tool_name=f"{server_name}__{remote_name}")
|
||||
return handler
|
||||
|
||||
# ==================== 公共 API ====================
|
||||
|
||||
@property
|
||||
|
||||
Reference in new issue
Block a user