@@ -429,6 +429,99 @@ async def test_mcp_normalization_function(mock_session, list_tool_calls_with_sla
429429 assert _normalize_mcp_name ("Name-With.Dots_And-Hyphens" ) == "Name-With.Dots_And-Hyphens"
430430
431431
432+ async def test_mcp_tool_name_collision_detected (caplog ):
433+ """Test that tools with names that normalize to the same identifier are detected and skipped."""
434+ plugin = MCPSsePlugin (name = "TestMCPPlugin" , url = "http://localhost:8080/sse" )
435+ session = AsyncMock (spec = ClientSession )
436+ session .list_tools .return_value = ListToolsResult (
437+ tools = [
438+ Tool (name = "read-document" , description = "first tool" , inputSchema = {}),
439+ Tool (name = "read document" , description = "second tool" , inputSchema = {}),
440+ ]
441+ )
442+ plugin .session = session
443+
444+ with caplog .at_level (logging .WARNING , logger = "semantic_kernel.connectors.mcp" ):
445+ await plugin .load_tools ()
446+
447+ # Only the first tool should be registered
448+ assert hasattr (plugin , "read-document" )
449+ func = getattr (plugin , "read-document" )
450+ assert func .__kernel_function_description__ == "first tool"
451+ # Warning should be emitted for the collision
452+ assert "read document" in caplog .text
453+ assert "already registered" in caplog .text
454+
455+
456+ async def test_mcp_prompt_name_collision_detected (caplog ):
457+ """Test that prompts with names that normalize to the same identifier are detected and skipped."""
458+ plugin = MCPSsePlugin (name = "TestMCPPlugin" , url = "http://localhost:8080/sse" )
459+ session = AsyncMock (spec = ClientSession )
460+ session .list_tools .return_value = ListToolsResult (tools = [])
461+ session .list_prompts .return_value = types .ListPromptsResult (
462+ prompts = [
463+ types .Prompt (name = "get-summary" , description = "first prompt" , arguments = []),
464+ types .Prompt (name = "get summary" , description = "second prompt" , arguments = []),
465+ ]
466+ )
467+ plugin .session = session
468+
469+ with caplog .at_level (logging .WARNING , logger = "semantic_kernel.connectors.mcp" ):
470+ await plugin .load_prompts ()
471+
472+ # Only the first prompt should be registered
473+ assert hasattr (plugin , "get-summary" )
474+ func = getattr (plugin , "get-summary" )
475+ assert func .__kernel_function_description__ == "first prompt"
476+ # Warning should be emitted for the collision
477+ assert "get summary" in caplog .text
478+ assert "already registered" in caplog .text
479+
480+
481+ async def test_mcp_tool_name_collision_detected_across_reload (caplog ):
482+ """Test that a later tool reload cannot overwrite a previously registered normalized name."""
483+ plugin = MCPSsePlugin (name = "TestMCPPlugin" , url = "http://localhost:8080/sse" )
484+ session = AsyncMock (spec = ClientSession )
485+ session .list_tools .side_effect = [
486+ ListToolsResult (tools = [Tool (name = "read-document" , description = "first tool" , inputSchema = {})]),
487+ ListToolsResult (tools = [Tool (name = "read document" , description = "second tool" , inputSchema = {})]),
488+ ]
489+ plugin .session = session
490+
491+ await plugin .load_tools ()
492+
493+ with caplog .at_level (logging .WARNING , logger = "semantic_kernel.connectors.mcp" ):
494+ await plugin .load_tools ()
495+
496+ func = getattr (plugin , "read-document" )
497+ assert func .__kernel_function_description__ == "first tool"
498+ assert "read document" in caplog .text
499+ assert "already registered" in caplog .text
500+
501+
502+ async def test_mcp_prompt_does_not_replace_registered_tool_name (caplog ):
503+ """Test that a prompt does not rebind a normalized name already registered by a tool."""
504+ plugin = MCPSsePlugin (name = "TestMCPPlugin" , url = "http://localhost:8080/sse" )
505+ session = AsyncMock (spec = ClientSession )
506+ session .list_tools .return_value = ListToolsResult (
507+ tools = [Tool (name = "read-document" , description = "first tool" , inputSchema = {})]
508+ )
509+ session .list_prompts .return_value = types .ListPromptsResult (
510+ prompts = [types .Prompt (name = "read document" , description = "second item" , arguments = [])]
511+ )
512+ plugin .session = session
513+
514+ await plugin .load_tools ()
515+
516+ with caplog .at_level (logging .WARNING , logger = "semantic_kernel.connectors.mcp" ):
517+ await plugin .load_prompts ()
518+
519+ func = getattr (plugin , "read-document" )
520+ assert func .__kernel_function_description__ == "first tool"
521+ assert "read document" in caplog .text
522+ assert "already registered" in caplog .text
523+
524+
432525async def test_excluded_function_cannot_be_called (kernel : "Kernel" ):
433526 """Test that excluded functions are rejected at call time, not just hidden from listing."""
434527 from semantic_kernel .connectors .mcp import create_mcp_server_from_kernel
0 commit comments