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:
@@ -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()
|
||||
|
||||
@@ -3,9 +3,15 @@ MaiSaka - MCP 管理器
|
||||
管理所有 MCP 服务器连接,提供统一的工具发现与调用接口。
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from src.cli.console import console
|
||||
from src.core.tooling import (
|
||||
ToolExecutionResult,
|
||||
ToolInvocation,
|
||||
ToolSpec,
|
||||
build_tool_detailed_description,
|
||||
)
|
||||
|
||||
from .config import DEFAULT_MCP_CONFIG_PATH, MCPServerConfig, load_mcp_config
|
||||
from .connection import MCPConnection, MCP_AVAILABLE
|
||||
@@ -74,7 +80,7 @@ class MCPManager:
|
||||
|
||||
# ──────── 连接管理 ────────
|
||||
|
||||
async def _connect_all(self, configs: list[MCPServerConfig]):
|
||||
async def _connect_all(self, configs: list[MCPServerConfig]) -> None:
|
||||
"""连接所有配置的 MCP 服务器,跳过失败的连接。"""
|
||||
for cfg in configs:
|
||||
conn = MCPConnection(cfg)
|
||||
@@ -112,44 +118,75 @@ class MCPManager:
|
||||
|
||||
# ──────── 工具发现 ────────
|
||||
|
||||
def get_openai_tools(self) -> list[dict]:
|
||||
"""
|
||||
将所有已注册的 MCP 工具转换为 OpenAI function calling 格式。
|
||||
def _build_tool_parameters_schema(self, tool: Any) -> dict[str, Any] | None:
|
||||
"""构造单个 MCP 工具的对象级参数 Schema。
|
||||
|
||||
Args:
|
||||
tool: MCP SDK 返回的原始工具对象。
|
||||
|
||||
Returns:
|
||||
OpenAI tools 格式的工具定义列表。
|
||||
dict[str, Any] | None: 参数 Schema。
|
||||
"""
|
||||
tools: list[dict] = []
|
||||
|
||||
parameters_schema = (
|
||||
dict(tool.inputSchema)
|
||||
if hasattr(tool, "inputSchema") and tool.inputSchema
|
||||
else {"type": "object", "properties": {}}
|
||||
)
|
||||
parameters_schema.pop("$schema", None)
|
||||
return parameters_schema
|
||||
|
||||
def get_tool_specs(self) -> list[ToolSpec]:
|
||||
"""获取全部已注册 MCP 工具的统一声明。
|
||||
|
||||
Returns:
|
||||
list[ToolSpec]: MCP 工具声明列表。
|
||||
"""
|
||||
|
||||
tool_specs: list[ToolSpec] = []
|
||||
for server_name, conn in self._connections.items():
|
||||
for tool in conn.tools:
|
||||
# 只包含成功注册的工具
|
||||
if tool.name not in self._tool_to_server:
|
||||
continue
|
||||
if self._tool_to_server[tool.name] != server_name:
|
||||
continue
|
||||
|
||||
# MCP inputSchema → OpenAI parameters
|
||||
parameters = (
|
||||
dict(tool.inputSchema)
|
||||
if hasattr(tool, "inputSchema") and tool.inputSchema
|
||||
else {"type": "object", "properties": {}}
|
||||
parameters_schema = self._build_tool_parameters_schema(tool)
|
||||
brief_description = str(tool.description or f"来自 {server_name} 的 MCP 工具").strip()
|
||||
tool_specs.append(
|
||||
ToolSpec(
|
||||
name=str(tool.name),
|
||||
brief_description=brief_description,
|
||||
detailed_description=build_tool_detailed_description(
|
||||
parameters_schema,
|
||||
fallback_description=f"工具来源:MCP 服务 {server_name}。",
|
||||
),
|
||||
parameters_schema=parameters_schema,
|
||||
provider_name="mcp",
|
||||
provider_type="mcp",
|
||||
metadata={"server_name": server_name},
|
||||
)
|
||||
)
|
||||
# 移除 $schema 字段(部分 MCP 服务器会带上,OpenAI 不接受)
|
||||
parameters.pop("$schema", None)
|
||||
return tool_specs
|
||||
|
||||
tools.append(
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool.name,
|
||||
"description": (tool.description or f"MCP tool from {server_name}"),
|
||||
"parameters": parameters,
|
||||
},
|
||||
}
|
||||
)
|
||||
def get_openai_tools(self) -> list[dict[str, Any]]:
|
||||
"""获取兼容旧模型层的 MCP 工具定义。
|
||||
|
||||
return tools
|
||||
Returns:
|
||||
list[dict[str, Any]]: OpenAI function tool 格式列表。
|
||||
"""
|
||||
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_spec.name,
|
||||
"description": tool_spec.build_llm_description(),
|
||||
"parameters": tool_spec.parameters_schema or {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
for tool_spec in self.get_tool_specs()
|
||||
]
|
||||
|
||||
# ──────── 工具调用 ────────
|
||||
|
||||
@@ -157,28 +194,46 @@ class MCPManager:
|
||||
"""判断工具名是否为已注册的 MCP 工具。"""
|
||||
return tool_name in self._tool_to_server
|
||||
|
||||
async def call_tool(self, tool_name: str, arguments: dict) -> str:
|
||||
"""
|
||||
调用指定的 MCP 工具。
|
||||
|
||||
自动路由到正确的 MCP 服务器。
|
||||
async def call_tool_invocation(self, invocation: ToolInvocation) -> ToolExecutionResult:
|
||||
"""执行统一的 MCP 工具调用。
|
||||
|
||||
Args:
|
||||
tool_name: 工具名称
|
||||
arguments: 工具参数
|
||||
invocation: 统一工具调用请求。
|
||||
|
||||
Returns:
|
||||
工具执行结果文本。
|
||||
ToolExecutionResult: 统一工具执行结果。
|
||||
"""
|
||||
|
||||
tool_name = invocation.tool_name
|
||||
server_name = self._tool_to_server.get(tool_name)
|
||||
if not server_name or server_name not in self._connections:
|
||||
return f"MCP 工具 '{tool_name}' 未找到"
|
||||
return ToolExecutionResult(
|
||||
tool_name=tool_name,
|
||||
success=False,
|
||||
error_message=f"MCP 工具 '{tool_name}' 未找到",
|
||||
)
|
||||
|
||||
conn = self._connections[server_name]
|
||||
try:
|
||||
return await conn.call_tool(tool_name, arguments)
|
||||
except Exception as e:
|
||||
return f"MCP 工具 '{tool_name}' 执行失败: {e}"
|
||||
return await conn.call_tool(tool_name, invocation.arguments)
|
||||
|
||||
async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> str:
|
||||
"""兼容旧接口,返回 MCP 工具的文本结果。
|
||||
|
||||
Args:
|
||||
tool_name: 工具名称。
|
||||
arguments: 工具参数。
|
||||
|
||||
Returns:
|
||||
str: 工具结果文本。
|
||||
"""
|
||||
|
||||
result = await self.call_tool_invocation(
|
||||
ToolInvocation(
|
||||
tool_name=tool_name,
|
||||
arguments=arguments,
|
||||
)
|
||||
)
|
||||
return result.get_history_content()
|
||||
|
||||
# ──────── 信息展示 ────────
|
||||
|
||||
@@ -207,7 +262,7 @@ class MCPManager:
|
||||
|
||||
# ──────── 生命周期 ────────
|
||||
|
||||
async def close(self):
|
||||
async def close(self) -> None:
|
||||
"""关闭所有 MCP 服务器连接。"""
|
||||
for conn in self._connections.values():
|
||||
await conn.close()
|
||||
|
||||
54
src/mcp_module/provider.py
Normal file
54
src/mcp_module/provider.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""MCP 工具 Provider。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from src.core.tooling import ToolExecutionContext, ToolExecutionResult, ToolInvocation, ToolProvider, ToolSpec
|
||||
|
||||
from .manager import MCPManager
|
||||
|
||||
|
||||
class MCPToolProvider(ToolProvider):
|
||||
"""基于 MCPManager 的工具 Provider。"""
|
||||
|
||||
provider_name = "mcp"
|
||||
provider_type = "mcp"
|
||||
|
||||
def __init__(self, manager: MCPManager) -> None:
|
||||
"""初始化 MCP 工具 Provider。
|
||||
|
||||
Args:
|
||||
manager: MCP 管理器实例。
|
||||
"""
|
||||
|
||||
self._manager = manager
|
||||
|
||||
async def list_tools(self) -> list[ToolSpec]:
|
||||
"""列出全部 MCP 工具。"""
|
||||
|
||||
return self._manager.get_tool_specs()
|
||||
|
||||
async def invoke(
|
||||
self,
|
||||
invocation: ToolInvocation,
|
||||
context: Optional[ToolExecutionContext] = None,
|
||||
) -> ToolExecutionResult:
|
||||
"""执行指定 MCP 工具。
|
||||
|
||||
Args:
|
||||
invocation: 工具调用请求。
|
||||
context: 执行上下文。
|
||||
|
||||
Returns:
|
||||
ToolExecutionResult: 工具执行结果。
|
||||
"""
|
||||
|
||||
del context
|
||||
return await self._manager.call_tool_invocation(invocation)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""关闭 Provider 并释放 MCP 连接。"""
|
||||
|
||||
await self._manager.close()
|
||||
|
||||
Reference in New Issue
Block a user