diff --git a/openhands-sdk/openhands/sdk/mcp/tool.py b/openhands-sdk/openhands/sdk/mcp/tool.py index 98697d0270..84fbc4a7a3 100644 --- a/openhands-sdk/openhands/sdk/mcp/tool.py +++ b/openhands-sdk/openhands/sdk/mcp/tool.py @@ -10,6 +10,7 @@ import mcp.types from litellm import ChatCompletionToolParam +from openai.types.responses import FunctionToolParam from pydantic import Field, ValidationError from openhands.sdk.logger import get_logger @@ -299,3 +300,33 @@ def to_openai_tool( add_security_risk_prediction=add_security_risk_prediction, action_type=mcp_action_type, ) + + def to_responses_tool( + self, + add_security_risk_prediction: bool = False, + action_type: type[Schema] | None = None, + ) -> FunctionToolParam: + """Convert a Tool to a Responses API function tool. + + For MCP, we dynamically create the action_type (type: Schema) + from the MCP tool input schema, and pass it to the parent method. + It will use the .model_fields from this pydantic model to + generate the Responses-compatible tool schema. + + Args: + add_security_risk_prediction: Whether to add a `security_risk` field + to the action schema for LLM to predict. This is useful for + tools that may have safety risks, so the LLM can reason about + the risk level before calling the tool. + """ + if action_type is not None: + raise ValueError( + "MCPTool.to_responses_tool does not support overriding action_type" + ) + + assert self.name == self.mcp_tool.name + mcp_action_type = _create_mcp_action_type(self.mcp_tool) + return super().to_responses_tool( + add_security_risk_prediction=add_security_risk_prediction, + action_type=mcp_action_type, + ) diff --git a/tests/sdk/mcp/test_create_mcp_tool.py b/tests/sdk/mcp/test_create_mcp_tool.py index a2dae2b32a..e4c63c0124 100644 --- a/tests/sdk/mcp/test_create_mcp_tool.py +++ b/tests/sdk/mcp/test_create_mcp_tool.py @@ -250,6 +250,19 @@ def test_create_mcp_tools_http_schema_validation(http_mcp_server: MCPTestServer) assert "a" in params["required"] assert "b" in params["required"] + responses_schema = add_tool.to_responses_tool() + responses_params = responses_schema["parameters"] + assert isinstance(responses_params, dict) + responses_properties = responses_params["properties"] + assert isinstance(responses_properties, dict) + responses_required = responses_params["required"] + assert isinstance(responses_required, list) + assert responses_properties["a"]["type"] == "integer" + assert responses_properties["b"]["type"] == "integer" + assert "a" in responses_required + assert "b" in responses_required + assert "data" not in responses_properties + def test_create_mcp_tools_transport_inferred_from_url(http_mcp_server: MCPTestServer): """Test that transport type is inferred when not explicitly specified.""" diff --git a/tests/sdk/mcp/test_mcp_security_risk.py b/tests/sdk/mcp/test_mcp_security_risk.py index bc3a7e85ea..edf33aedf0 100644 --- a/tests/sdk/mcp/test_mcp_security_risk.py +++ b/tests/sdk/mcp/test_mcp_security_risk.py @@ -95,6 +95,38 @@ def test_mcp_tool_to_openai_with_security_risk(): ) +def test_mcp_tool_to_responses_with_security_risk(): + """Test that MCP Responses schema includes security_risk correctly.""" + mcp_tool_def = mcp.types.Tool( + name="fetch_fetch", + description="Fetch a URL", + inputSchema={ + "type": "object", + "properties": {"url": {"type": "string", "description": "URL to fetch"}}, + "required": ["url"], + }, + ) + + mock_client = MockMCPClient() + tools = MCPToolDefinition.create(mcp_tool=mcp_tool_def, mcp_client=mock_client) + tool = tools[0] + + responses_tool = tool.to_responses_tool(add_security_risk_prediction=True) + + params = responses_tool["parameters"] + assert isinstance(params, dict) + properties = params["properties"] + assert isinstance(properties, dict) + required = params.get("required", []) + assert isinstance(required, list) + + assert "url" in properties + assert "security_risk" in properties + assert "data" not in properties + assert "url" in required + assert "security_risk" not in required + + def test_mcp_tool_action_from_arguments_with_security_risk(): """Test that action_from_arguments works correctly with security_risk popped.