|
1 | 1 | # Copyright (c) Microsoft. All rights reserved.
|
| 2 | + |
| 3 | +import re |
2 | 4 | from typing import TYPE_CHECKING
|
3 | 5 | from unittest.mock import AsyncMock, MagicMock, patch
|
4 | 6 |
|
|
12 | 14 | from semantic_kernel import Kernel
|
13 | 15 |
|
14 | 16 |
|
| 17 | +@pytest.fixture |
| 18 | +def list_tool_calls_with_slash() -> ListToolsResult: |
| 19 | + return ListToolsResult( |
| 20 | + tools=[ |
| 21 | + Tool( |
| 22 | + name="nasa/get-astronomy-picture", |
| 23 | + description="func with slash", |
| 24 | + inputSchema={"properties": {}, "required": []}, |
| 25 | + ), |
| 26 | + Tool( |
| 27 | + name="weird\\name with spaces", |
| 28 | + description="func with backslash and spaces", |
| 29 | + inputSchema={"properties": {}, "required": []}, |
| 30 | + ), |
| 31 | + ] |
| 32 | + ) |
| 33 | + |
| 34 | + |
15 | 35 | @pytest.fixture
|
16 | 36 | def list_tool_calls() -> ListToolsResult:
|
17 | 37 | return ListToolsResult(
|
@@ -230,3 +250,44 @@ async def test_kernel_as_mcp_server(kernel: "Kernel", decorated_native_function,
|
230 | 250 | assert types.ListToolsRequest in server.request_handlers
|
231 | 251 | assert types.CallToolRequest in server.request_handlers
|
232 | 252 | assert server.name == "Semantic Kernel MCP Server"
|
| 253 | + |
| 254 | + |
| 255 | +@patch("semantic_kernel.connectors.mcp.sse_client") |
| 256 | +@patch("semantic_kernel.connectors.mcp.ClientSession") |
| 257 | +async def test_mcp_tool_name_normalization(mock_session, mock_client, list_tool_calls_with_slash, kernel: "Kernel"): |
| 258 | + """Test that MCP tool names with illegal characters are normalized.""" |
| 259 | + mock_read = MagicMock() |
| 260 | + mock_write = MagicMock() |
| 261 | + mock_generator = MagicMock() |
| 262 | + mock_generator.__aenter__.return_value = (mock_read, mock_write) |
| 263 | + mock_generator.__aexit__.return_value = (mock_read, mock_write) |
| 264 | + mock_client.return_value = mock_generator |
| 265 | + mock_session.return_value.__aenter__.return_value.list_tools.return_value = list_tool_calls_with_slash |
| 266 | + |
| 267 | + async with MCPSsePlugin( |
| 268 | + name="TestMCPPlugin", |
| 269 | + description="Test MCP Plugin", |
| 270 | + url="http://localhost:8080/sse", |
| 271 | + ) as plugin: |
| 272 | + loaded_plugin = kernel.add_plugin(plugin) |
| 273 | + # The normalized names: |
| 274 | + assert "nasa-get-astronomy-picture" in loaded_plugin.functions |
| 275 | + assert "weird-name-with-spaces" in loaded_plugin.functions |
| 276 | + # They should not exist with their original (invalid) names: |
| 277 | + assert "nasa/get-astronomy-picture" not in loaded_plugin.functions |
| 278 | + assert "weird\\name with spaces" not in loaded_plugin.functions |
| 279 | + |
| 280 | + normalized_names = list(loaded_plugin.functions.keys()) |
| 281 | + for name in normalized_names: |
| 282 | + assert re.match(r"^[A-Za-z0-9_.-]+$", name) |
| 283 | + |
| 284 | + |
| 285 | +@patch("semantic_kernel.connectors.mcp.ClientSession") |
| 286 | +async def test_mcp_normalization_function(mock_session, list_tool_calls_with_slash): |
| 287 | + """Unit test for the normalize_mcp_name function (should exist in codebase).""" |
| 288 | + from semantic_kernel.connectors.mcp import _normalize_mcp_name |
| 289 | + |
| 290 | + assert _normalize_mcp_name("nasa/get-astronomy-picture") == "nasa-get-astronomy-picture" |
| 291 | + assert _normalize_mcp_name("weird\\name with spaces") == "weird-name-with-spaces" |
| 292 | + assert _normalize_mcp_name("simple_name") == "simple_name" |
| 293 | + assert _normalize_mcp_name("Name-With.Dots_And-Hyphens") == "Name-With.Dots_And-Hyphens" |
0 commit comments