feat: add MCP tool integration
This commit is contained in:
@@ -14,6 +14,7 @@ from services.runtime_variables import (
|
||||
DynamicVariableStore,
|
||||
value_at_path,
|
||||
)
|
||||
from services.tools import McpClientError, McpToolClient
|
||||
|
||||
|
||||
class ToolExecutionError(RuntimeError):
|
||||
@@ -21,8 +22,16 @@ class ToolExecutionError(RuntimeError):
|
||||
|
||||
|
||||
class ToolExecutor:
|
||||
def __init__(self, store: DynamicVariableStore):
|
||||
"""Execute supported tools and apply their results to session variables."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
store: DynamicVariableStore,
|
||||
*,
|
||||
mcp_client: McpToolClient | None = None,
|
||||
):
|
||||
self.store = store
|
||||
self._mcp_client = mcp_client or McpToolClient()
|
||||
|
||||
def register_secrets(self, tool: RuntimeTool) -> None:
|
||||
dynamic = (tool.secrets or {}).get("dynamic_variables") or {}
|
||||
@@ -34,6 +43,12 @@ class ToolExecutor:
|
||||
@staticmethod
|
||||
def schema_parts(tool: RuntimeTool) -> tuple[dict[str, Any], list[str]]:
|
||||
config = (tool.definition or {}).get("config") or {}
|
||||
if tool.type == "mcp":
|
||||
schema = config.get("input_schema") or {}
|
||||
return (
|
||||
dict(schema.get("properties") or {}),
|
||||
[str(name) for name in schema.get("required") or []],
|
||||
)
|
||||
parameters = list(config.get("parameters") or [])
|
||||
properties = {
|
||||
str(parameter.get("name")): {
|
||||
@@ -58,11 +73,16 @@ class ToolExecutor:
|
||||
result_assignments: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
self.register_secrets(tool)
|
||||
if tool.type != "http":
|
||||
raise ToolExecutionError(f"Action 暂不支持工具类型: {tool.type}")
|
||||
return await self._execute_http(
|
||||
normalized_arguments = dict(arguments or {})
|
||||
if tool.type == "http":
|
||||
result = await self._execute_http(tool, normalized_arguments)
|
||||
elif tool.type == "mcp":
|
||||
result = await self._execute_mcp(tool, normalized_arguments)
|
||||
else:
|
||||
raise ToolExecutionError(f"不支持工具类型: {tool.type}")
|
||||
return self._apply_result_assignments(
|
||||
tool,
|
||||
dict(arguments or {}),
|
||||
result,
|
||||
result_assignments=result_assignments,
|
||||
)
|
||||
|
||||
@@ -70,8 +90,6 @@ class ToolExecutor:
|
||||
self,
|
||||
tool: RuntimeTool,
|
||||
arguments: dict[str, Any],
|
||||
*,
|
||||
result_assignments: dict[str, str] | None,
|
||||
) -> dict[str, Any]:
|
||||
config = (tool.definition or {}).get("config") or {}
|
||||
parameters = list(config.get("parameters") or [])
|
||||
@@ -130,11 +148,59 @@ class ToolExecutor:
|
||||
except ValueError:
|
||||
payload = {"text": response.text[:8000]}
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"status_code": response.status_code,
|
||||
"data": payload,
|
||||
}
|
||||
|
||||
async def _execute_mcp(
|
||||
self,
|
||||
tool: RuntimeTool,
|
||||
arguments: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
server = tool.mcp_server
|
||||
if server is None:
|
||||
raise ToolExecutionError("MCP 工具缺少可用的 Server 连接")
|
||||
config = (tool.definition or {}).get("config") or {}
|
||||
remote_tool_name = str(config.get("remote_tool_name") or "")
|
||||
if not remote_tool_name:
|
||||
raise ToolExecutionError("MCP 工具缺少远端工具名称")
|
||||
|
||||
resolved_server = server.model_copy(
|
||||
update={
|
||||
"url": self.store.render(server.url),
|
||||
"headers": self.store.render_data(server.headers),
|
||||
"secret_headers": self.store.render_data(
|
||||
server.secret_headers,
|
||||
allow_secrets=True,
|
||||
),
|
||||
}
|
||||
)
|
||||
try:
|
||||
payload = await self._mcp_client.call_tool(
|
||||
resolved_server,
|
||||
remote_tool_name,
|
||||
arguments,
|
||||
)
|
||||
except McpClientError as exc:
|
||||
raise ToolExecutionError(str(exc)) from exc
|
||||
return {"status": "ok", "data": payload}
|
||||
|
||||
def _apply_result_assignments(
|
||||
self,
|
||||
tool: RuntimeTool,
|
||||
result: dict[str, Any],
|
||||
*,
|
||||
result_assignments: dict[str, str] | None,
|
||||
) -> dict[str, Any]:
|
||||
config = (tool.definition or {}).get("config") or {}
|
||||
assignments = (
|
||||
result_assignments
|
||||
if result_assignments is not None
|
||||
else config.get("dynamic_variable_assignments") or {}
|
||||
)
|
||||
payload = result.get("data")
|
||||
updated: list[str] = []
|
||||
for variable_name, path in assignments.items():
|
||||
try:
|
||||
@@ -146,9 +212,4 @@ class ToolExecutor:
|
||||
continue
|
||||
self.store.assign(str(variable_name), value)
|
||||
updated.append(str(variable_name))
|
||||
return {
|
||||
"status": "ok",
|
||||
"status_code": response.status_code,
|
||||
"data": payload,
|
||||
"updated_variables": updated,
|
||||
}
|
||||
return {**result, "updated_variables": updated}
|
||||
|
||||
Reference in New Issue
Block a user