diff --git a/api/core/agent/fc_agent_runner.py b/api/core/agent/fc_agent_runner.py index 29de0b8b1c..89d574b29e 100644 --- a/api/core/agent/fc_agent_runner.py +++ b/api/core/agent/fc_agent_runner.py @@ -236,6 +236,9 @@ class FunctionCallAgentRunner(BaseAgentRunner): "meta": ToolInvokeMeta.error_instance(f"there is not a tool named {tool_call_name}").to_dict(), } else: + inputs_to_pass = ( + kwargs.get("inputs") if tool_instance.tool_provider_type() == ToolProviderType.MCP else None + ) # invoke tool tool_invoke_response, message_files, tool_invoke_meta = ToolEngine.agent_invoke( tool=tool_instance, @@ -249,6 +252,7 @@ class FunctionCallAgentRunner(BaseAgentRunner): app_id=self.application_generate_entity.app_config.app_id, message_id=self.message.id, conversation_id=self.conversation.id, + inputs=inputs_to_pass, ) # publish files for message_file_id in message_files: diff --git a/api/core/mcp/auth_client.py b/api/core/mcp/auth_client.py index 173913196e..5e6cdbccca 100644 --- a/api/core/mcp/auth_client.py +++ b/api/core/mcp/auth_client.py @@ -180,7 +180,9 @@ class MCPClientWithAuthRetry(MCPClient): """ return self._execute_with_retry(super().list_tools) - def invoke_tool(self, tool_name: str, tool_args: dict[str, Any]) -> CallToolResult: + def invoke_tool( + self, tool_name: str, tool_args: dict[str, Any], _meta: dict[str, Any] | None = None + ) -> CallToolResult: """ Invoke a tool on the MCP server with auth retry. @@ -194,4 +196,4 @@ class MCPClientWithAuthRetry(MCPClient): Raises: MCPAuthError: If authentication fails after retries """ - return self._execute_with_retry(super().invoke_tool, tool_name, tool_args) + return self._execute_with_retry(super().invoke_tool, tool_name, tool_args, _meta) diff --git a/api/core/mcp/mcp_client.py b/api/core/mcp/mcp_client.py index 2b0645b558..2b30bfc21b 100644 --- a/api/core/mcp/mcp_client.py +++ b/api/core/mcp/mcp_client.py @@ -96,11 +96,13 @@ class MCPClient: response = self._session.list_tools() return response.tools - def invoke_tool(self, tool_name: str, tool_args: dict[str, Any]) -> CallToolResult: + def invoke_tool( + self, tool_name: str, tool_args: dict[str, Any], _meta: dict[str, Any] | None = None + ) -> CallToolResult: """Call a tool""" if not self._session: raise ValueError("Session not initialized.") - return self._session.call_tool(tool_name, tool_args) + return self._session.call_tool(tool_name, tool_args, _meta=_meta) def cleanup(self): """Clean up resources""" diff --git a/api/core/mcp/session/client_session.py b/api/core/mcp/session/client_session.py index d684fe0dd7..33be00365e 100644 --- a/api/core/mcp/session/client_session.py +++ b/api/core/mcp/session/client_session.py @@ -248,6 +248,7 @@ class ClientSession( self, name: str, arguments: dict[str, Any] | None = None, + _meta: dict[str, Any] | None = None, read_timeout_seconds: timedelta | None = None, ) -> types.CallToolResult: """Send a tools/call request.""" @@ -256,7 +257,7 @@ class ClientSession( types.ClientRequest( types.CallToolRequest( method="tools/call", - params=types.CallToolRequestParams(name=name, arguments=arguments), + params=types.CallToolRequestParams(name=name, arguments=arguments, _meta=_meta), ) ), types.CallToolResult, diff --git a/api/core/mcp/types.py b/api/core/mcp/types.py index 10e3082aa3..ac8e8c847b 100644 --- a/api/core/mcp/types.py +++ b/api/core/mcp/types.py @@ -849,6 +849,7 @@ class CallToolRequestParams(RequestParams): name: str arguments: dict[str, Any] | None = None + _meta: dict[str, Any] | None = None model_config = ConfigDict(extra="allow") diff --git a/api/core/tools/__base/tool.py b/api/core/tools/__base/tool.py index ab0f73a9a2..17d6500485 100644 --- a/api/core/tools/__base/tool.py +++ b/api/core/tools/__base/tool.py @@ -51,6 +51,7 @@ class Tool(ABC): conversation_id: str | None = None, app_id: str | None = None, message_id: str | None = None, + inputs: dict[str, Any] | None = None, ) -> Generator[ToolInvokeMessage]: if self.runtime and self.runtime.runtime_parameters: tool_parameters.update(self.runtime.runtime_parameters) @@ -58,13 +59,24 @@ class Tool(ABC): # try parse tool parameters into the correct type tool_parameters = self._transform_tool_parameters_type(tool_parameters) - result = self._invoke( - user_id=user_id, - tool_parameters=tool_parameters, - conversation_id=conversation_id, - app_id=app_id, - message_id=message_id, - ) + # Construct the call parameter and pass in inputs only when the _invoke of the subclass accepts inputs + invoke_kwargs = { + "user_id": user_id, + "tool_parameters": tool_parameters, + "conversation_id": conversation_id, + "app_id": app_id, + "message_id": message_id, + } + if inputs is not None: + try: + sig = inspect.signature(self._invoke) + if "inputs" in sig.parameters: + invoke_kwargs["inputs"] = inputs + except (ValueError, TypeError): + # fallback: Do not pass inputs if reflection fails + pass + + result = self._invoke(**invoke_kwargs) if isinstance(result, ToolInvokeMessage): diff --git a/api/core/tools/mcp_tool/tool.py b/api/core/tools/mcp_tool/tool.py index 00fc8a8282..67d27ce84d 100644 --- a/api/core/tools/mcp_tool/tool.py +++ b/api/core/tools/mcp_tool/tool.py @@ -59,9 +59,10 @@ class MCPTool(Tool): conversation_id: str | None = None, app_id: str | None = None, message_id: str | None = None, + inputs: dict[str, Any] | None = None, ) -> Generator[ToolInvokeMessage, None, None]: - result = self.invoke_remote_mcp_tool(tool_parameters) - + result = self.invoke_remote_mcp_tool(tool_parameters, _meta=inputs) + # Extract usage metadata from MCP protocol's _meta field self._latest_usage = self._derive_usage_from_result(result) @@ -242,7 +243,7 @@ class MCPTool(Tool): if value is not None and not (isinstance(value, str) and value.strip() == "") } - def invoke_remote_mcp_tool(self, tool_parameters: dict[str, Any]) -> CallToolResult: + def invoke_remote_mcp_tool(self, tool_parameters: dict[str, Any], _meta: dict[str, Any] | None) -> CallToolResult: headers = self.headers.copy() if self.headers else {} tool_parameters = self._handle_none_parameter(tool_parameters) @@ -277,7 +278,9 @@ class MCPTool(Tool): sse_read_timeout=self.sse_read_timeout, provider_entity=provider_entity, ) as mcp_client: - return mcp_client.invoke_tool(tool_name=self.entity.identity.name, tool_args=tool_parameters) + return mcp_client.invoke_tool( + tool_name=self.entity.identity.name, tool_args=tool_parameters, _meta=_meta + ) except MCPConnectionError as e: raise ToolInvokeError(f"Failed to connect to MCP server: {e}") from e except Exception as e: diff --git a/api/core/tools/tool_engine.py b/api/core/tools/tool_engine.py index 3caacb8706..f6a4702540 100644 --- a/api/core/tools/tool_engine.py +++ b/api/core/tools/tool_engine.py @@ -57,6 +57,7 @@ class ToolEngine: conversation_id: str | None = None, app_id: str | None = None, message_id: str | None = None, + inputs: dict[str, Any] | None = None, ) -> tuple[str, list[str], ToolInvokeMeta]: """ Agent invokes the tool with the given arguments. @@ -81,7 +82,7 @@ class ToolEngine: # hit the callback handler agent_tool_callback.on_tool_start(tool_name=tool.entity.identity.name, tool_inputs=tool_parameters) - messages = ToolEngine._invoke(tool, tool_parameters, user_id, conversation_id, app_id, message_id) + messages = ToolEngine._invoke(tool, tool_parameters, user_id, conversation_id, app_id, message_id, inputs) invocation_meta_dict: dict[str, ToolInvokeMeta] = {} def message_callback( @@ -206,6 +207,7 @@ class ToolEngine: conversation_id: str | None = None, app_id: str | None = None, message_id: str | None = None, + inputs: dict[str, Any] | None = None, ) -> Generator[ToolInvokeMessage | ToolInvokeMeta, None, None]: """ Invoke the tool with the given arguments. @@ -223,7 +225,7 @@ class ToolEngine: }, ) try: - yield from tool.invoke(user_id, tool_parameters, conversation_id, app_id, message_id) + yield from tool.invoke(user_id, tool_parameters, conversation_id, app_id, message_id, inputs) except Exception as e: meta.error = str(e) raise ToolEngineInvokeError(meta)