@@ -406,3 +406,54 @@ async def test_mcp_normalization_function(mock_session, list_tool_calls_with_sla
406406 assert _normalize_mcp_name ("weird\\ name with spaces" ) == "weird-name-with-spaces"
407407 assert _normalize_mcp_name ("simple_name" ) == "simple_name"
408408 assert _normalize_mcp_name ("Name-With.Dots_And-Hyphens" ) == "Name-With.Dots_And-Hyphens"
409+
410+
411+ async def test_excluded_function_cannot_be_called (kernel : "Kernel" ):
412+ """Test that excluded functions are rejected at call time, not just hidden from listing."""
413+ from semantic_kernel .connectors .mcp import create_mcp_server_from_kernel
414+ from semantic_kernel .functions .kernel_function_decorator import kernel_function
415+
416+ side_effect_called = False
417+
418+ @kernel_function (name = "public_echo" )
419+ def public_echo (message : str ) -> str :
420+ return f"echo: { message } "
421+
422+ @kernel_function (name = "secret_admin" )
423+ def secret_admin (target : str ) -> str :
424+ nonlocal side_effect_called
425+ side_effect_called = True
426+ return f"privileged action on { target } "
427+
428+ kernel .add_function (plugin_name = "tools" , function = public_echo )
429+ kernel .add_function (plugin_name = "tools" , function = secret_admin )
430+
431+ server = create_mcp_server_from_kernel (kernel , excluded_functions = ["secret_admin" ])
432+
433+ # Verify the server was created with handlers
434+ assert types .ListToolsRequest in server .request_handlers
435+ assert types .CallToolRequest in server .request_handlers
436+
437+ # Mock _get_cached_tool_definition to bypass SDK request context requirements
438+ # (normally set by a real MCP session transport)
439+ async def _fake_get_cached_tool_definition (tool_name ):
440+ return None
441+
442+ server ._get_cached_tool_definition = _fake_get_cached_tool_definition
443+
444+ # Build a proper CallToolRequest as the MCP SDK would send
445+ call_tool_request = types .CallToolRequest (
446+ method = "tools/call" ,
447+ params = types .CallToolRequestParams (name = "secret_admin" , arguments = {}),
448+ )
449+
450+ # The internal handler wraps our _call_tool; invoke via the registered handler
451+ handler = server .request_handlers [types .CallToolRequest ]
452+ result = await handler (call_tool_request )
453+
454+ # The call must fail (isError=True) with the correct error message
455+ assert result .root .isError is True , "Calling an excluded function should return an error"
456+ assert any (
457+ "Unknown tool" in c .text for c in result .root .content if hasattr (c , "text" )
458+ ), f"Expected 'Unknown tool' error, got: { result .root .content } "
459+ assert not side_effect_called , "Excluded function's side effect should not have fired"
0 commit comments