feat: Introduce unified tooling system for plugins and MCP

- Added a new `tooling` module to define a unified model for tool declarations, invocations, and execution results, facilitating compatibility between plugins, legacy actions, and MCP tools.
- Implemented `ToolProvider` interface for various tool providers including built-in tools, MCP tools, and plugin runtime tools.
- Enhanced `MCPManager` and `MCPConnection` to support unified tool invocation and execution results.
- Updated `ComponentRegistry` and related classes to accommodate the new tool specifications and descriptions.
- Refactored existing components to utilize the new tooling system, ensuring backward compatibility with legacy actions.
- Improved error handling and logging for tool invocations across different providers.
This commit is contained in:
DrSmoothl
2026-03-30 23:11:56 +08:00
parent 898b693fe0
commit dc2bf02a42
35 changed files with 1663 additions and 6756 deletions

View File

@@ -6,6 +6,8 @@ MaiSaka - 单个 MCP 服务器连接管理
from contextlib import AsyncExitStack
from typing import Any, Optional
from src.core.tooling import ToolExecutionResult
from src.cli.console import console
from .config import MCPServerConfig
@@ -79,6 +81,8 @@ class MCPConnection:
return False
# 创建并初始化 MCP 会话
if ClientSession is None:
raise RuntimeError("当前环境未安装可用的 MCP ClientSession")
self.session = await self._exit_stack.enter_async_context(ClientSession(read_stream, write_stream))
await self.session.initialize()
@@ -95,6 +99,11 @@ class MCPConnection:
async def _connect_stdio(self):
"""建立 Stdio 传输连接。"""
if StdioServerParameters is None or stdio_client is None:
raise RuntimeError("当前环境未安装可用的 MCP stdio 客户端")
if not self.config.command:
raise ValueError(f"MCP 服务器 '{self.config.name}' 缺少 stdio command 配置")
params = StdioServerParameters(
command=self.config.command,
args=self.config.args,
@@ -106,39 +115,63 @@ class MCPConnection:
"""建立 SSE 传输连接。"""
if not SSE_AVAILABLE:
raise ImportError("SSE 传输需要额外依赖,请运行: pip install mcp[sse]")
if sse_client is None:
raise RuntimeError("当前环境未安装可用的 MCP SSE 客户端")
if not self.config.url:
raise ValueError(f"MCP 服务器 '{self.config.name}' 缺少 SSE url 配置")
return await self._exit_stack.enter_async_context(sse_client(url=self.config.url, headers=self.config.headers))
async def call_tool(self, tool_name: str, arguments: dict) -> str:
"""
调用 MCP 工具并返回结果文本。
async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> ToolExecutionResult:
"""调用 MCP 工具并返回统一执行结果。
Args:
tool_name: 工具名称
arguments: 工具参数字典
tool_name: 工具名称
arguments: 工具参数字典
Returns:
工具执行结果的文本表示
ToolExecutionResult: 统一执行结果
"""
if not self.session:
return f"MCP 服务器 '{self.config.name}' 未连接"
return ToolExecutionResult(
tool_name=tool_name,
success=False,
error_message=f"MCP 服务器 '{self.config.name}' 未连接",
)
result = await self.session.call_tool(tool_name, arguments=arguments)
try:
result = await self.session.call_tool(tool_name, arguments=arguments)
except Exception as exc:
return ToolExecutionResult(
tool_name=tool_name,
success=False,
error_message=f"MCP 工具 '{tool_name}' 执行失败: {exc}",
metadata={"server_name": self.config.name},
)
# 将结果内容转换为文本
parts: list[str] = []
text_parts: list[str] = []
binary_parts: list[dict[str, Any]] = []
for content in result.content:
if hasattr(content, "text"):
parts.append(content.text)
text_parts.append(str(content.text))
elif hasattr(content, "data"):
# 二进制/图片内容,展示类型信息
content_type = getattr(content, "mimeType", "unknown")
parts.append(f"[{content_type} 二进制内容]")
binary_parts.append({"mime_type": content_type, "type": "binary"})
text_parts.append(f"[{content_type} 二进制内容]")
elif hasattr(content, "type"):
parts.append(f"[{content.type} 内容]")
text_parts.append(f"[{content.type} 内容]")
return "\n".join(parts) if parts else "工具执行成功(无输出)"
return ToolExecutionResult(
tool_name=tool_name,
success=True,
content="\n".join(text_parts) if text_parts else "工具执行成功(无输出)",
metadata={
"server_name": self.config.name,
"binary_parts": binary_parts,
},
)
async def close(self):
async def close(self) -> None:
"""关闭连接并释放资源。"""
try:
await self._exit_stack.aclose()