@@ -46,9 +46,11 @@ def __init__(
4646 name = "test_tool" ,
4747 description = "Test tool description" ,
4848 outputSchema = None ,
49+ meta = None ,
4950 ):
5051 self .name = name
5152 self .description = description
53+ self .meta = meta
5254 self .inputSchema = {
5355 "type" : "object" ,
5456 "properties" : {
@@ -213,7 +215,8 @@ async def test_run_async_impl_no_auth(self):
213215 )
214216 self .mock_session .call_tool = AsyncMock (return_value = mcp_response )
215217
216- tool_context = Mock (spec = ToolContext )
218+ tool_context = ToolContext (invocation_context = Mock ())
219+ tool_context .function_call_id = "test-call-id"
217220 args = {"param1" : "test_value" }
218221
219222 result = await tool ._run_async_impl (
@@ -230,6 +233,42 @@ async def test_run_async_impl_no_auth(self):
230233 "test_tool" , arguments = args , progress_callback = None , meta = None
231234 )
232235
236+ @pytest .mark .asyncio
237+ async def test_run_async_impl_adds_ui_widget (self ):
238+ """Test running tool adds UiWidget to actions."""
239+ meta = {"ui" : {"resourceUri" : "ui://test-app" }}
240+ mock_tool = MockMCPTool (meta = meta )
241+ tool = MCPTool (
242+ mcp_tool = mock_tool ,
243+ mcp_session_manager = self .mock_session_manager ,
244+ )
245+
246+ mcp_response = CallToolResult (
247+ content = [TextContent (type = "text" , text = "success" )]
248+ )
249+ self .mock_session .call_tool = AsyncMock (return_value = mcp_response )
250+
251+ tool_context = ToolContext (invocation_context = Mock ())
252+ tool_context .function_call_id = "test-call-id"
253+ args = {"param1" : "test_value" }
254+
255+ # tool_context.actions.render_ui_widgets is None initially
256+ result = await tool ._run_async_impl (
257+ args = args , tool_context = tool_context , credential = None
258+ )
259+
260+ assert result == mcp_response .model_dump (exclude_none = True , mode = "json" )
261+
262+ assert tool_context .actions .render_ui_widgets is not None
263+ assert len (tool_context .actions .render_ui_widgets ) == 1
264+ widget = tool_context .actions .render_ui_widgets [0 ]
265+
266+ assert widget .id == "test-call-id"
267+ assert widget .provider == "mcp"
268+ assert widget .payload ["resource_uri" ] == "ui://test-app"
269+ assert widget .payload ["tool" ] == mock_tool
270+ assert widget .payload ["tool_args" ] == args
271+
233272 @pytest .mark .asyncio
234273 async def test_run_async_impl_with_oauth2 (self ):
235274 """Test running tool with OAuth2 authentication."""
@@ -1015,3 +1054,76 @@ async def _require_confirmation_func(param1: str, ctx: Context):
10151054 )
10161055 }
10171056 tool_context .request_confirmation .assert_called_once ()
1057+
1058+ def test_visibility_property (self ):
1059+ """Test visibility property extraction from meta."""
1060+ meta = {"ui" : {"visibility" : ["app" , "debug" ]}}
1061+ mock_tool = MockMCPTool (meta = meta )
1062+ tool = MCPTool (
1063+ mcp_tool = mock_tool ,
1064+ mcp_session_manager = self .mock_session_manager ,
1065+ )
1066+
1067+ assert tool .visibility == ["app" , "debug" ]
1068+
1069+ def test_visibility_property_empty (self ):
1070+ """Test visibility property when meta is missing or malformed."""
1071+ # Missing meta
1072+ tool1 = MCPTool (
1073+ mcp_tool = MockMCPTool (meta = None ),
1074+ mcp_session_manager = self .mock_session_manager ,
1075+ )
1076+ assert tool1 .visibility == []
1077+
1078+ # Malformed meta
1079+ tool2 = MCPTool (
1080+ mcp_tool = MockMCPTool (meta = "not a dict" ),
1081+ mcp_session_manager = self .mock_session_manager ,
1082+ )
1083+ assert tool2 .visibility == []
1084+
1085+ # Missing ui field
1086+ tool3 = MCPTool (
1087+ mcp_tool = MockMCPTool (meta = {}),
1088+ mcp_session_manager = self .mock_session_manager ,
1089+ )
1090+ assert tool3 .visibility == []
1091+
1092+ def test_mcp_app_resource_uri_property_nested (self ):
1093+ """Test MCP App resource URI extraction from nested meta format."""
1094+ meta = {"ui" : {"resourceUri" : "ui://test-resource" }}
1095+ mock_tool = MockMCPTool (meta = meta )
1096+ tool = MCPTool (
1097+ mcp_tool = mock_tool ,
1098+ mcp_session_manager = self .mock_session_manager ,
1099+ )
1100+
1101+ assert tool .mcp_app_resource_uri == "ui://test-resource"
1102+
1103+ def test_mcp_app_resource_uri_property_flat (self ):
1104+ """Test MCP App resource URI extraction from flat meta format."""
1105+ meta = {"ui/resourceUri" : "ui://test-resource-flat" }
1106+ mock_tool = MockMCPTool (meta = meta )
1107+ tool = MCPTool (
1108+ mcp_tool = mock_tool ,
1109+ mcp_session_manager = self .mock_session_manager ,
1110+ )
1111+
1112+ assert tool .mcp_app_resource_uri == "ui://test-resource-flat"
1113+
1114+ def test_mcp_app_resource_uri_property_none (self ):
1115+ """Test MCP App resource URI when missing or invalid."""
1116+ # Missing meta
1117+ tool1 = MCPTool (
1118+ mcp_tool = MockMCPTool (meta = None ),
1119+ mcp_session_manager = self .mock_session_manager ,
1120+ )
1121+ assert tool1 .mcp_app_resource_uri is None
1122+
1123+ # Invalid scheme
1124+ meta = {"ui" : {"resourceUri" : "http://invalid" }}
1125+ tool2 = MCPTool (
1126+ mcp_tool = MockMCPTool (meta = meta ),
1127+ mcp_session_manager = self .mock_session_manager ,
1128+ )
1129+ assert tool2 .mcp_app_resource_uri is None
0 commit comments