Skip to content

Commit cd5a1df

Browse files
improved parsing of schema and tests
1 parent 055b661 commit cd5a1df

File tree

3 files changed

+87
-40
lines changed

3 files changed

+87
-40
lines changed

python/semantic_kernel/connectors/mcp.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
from functools import partial
77
from typing import Any
88

9-
from mcp import ClientSession, McpError, StdioServerParameters, stdio_client
9+
from mcp import McpError
10+
from mcp.client.session import ClientSession
1011
from mcp.client.sse import sse_client
12+
from mcp.client.stdio import StdioServerParameters, stdio_client
1113
from mcp.types import Tool
1214
from pydantic import BaseModel, ConfigDict, Field
1315

@@ -157,11 +159,7 @@ def get_parameters_from_tool(tool: Tool) -> list[KernelParameterMetadata]:
157159
is_required=prop_name in required,
158160
type=prop_details.get("type"),
159161
default_value=prop_details.get("default", None),
160-
schema_data=prop_details["items"]
161-
if "items" in prop_details and prop_details["items"] is not None and isinstance(prop_details["items"], dict)
162-
else {"type": f"{prop_details['type']}"}
163-
if "type" in prop_details
164-
else None,
162+
schema_data=prop_details,
165163
)
166164
for prop_name, prop_details in properties.items()
167165
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
4+
import os
5+
from typing import TYPE_CHECKING
6+
7+
from semantic_kernel.connectors.mcp import MCPStdioServerConfig, create_plugin_from_mcp_server
8+
from semantic_kernel.functions.kernel_arguments import KernelArguments
9+
10+
if TYPE_CHECKING:
11+
from semantic_kernel import Kernel
12+
13+
14+
async def test_from_mcp(kernel: "Kernel"):
15+
mcp_server_path = os.path.join(os.path.dirname(__file__), "../../assets/test_plugins", "TestMCPPlugin")
16+
mcp_server_file = "mcp_server.py"
17+
config = MCPStdioServerConfig(
18+
command="uv",
19+
args=["--directory", mcp_server_path, "run", mcp_server_file],
20+
)
21+
22+
plugin = await create_plugin_from_mcp_server(
23+
plugin_name="TestMCPPlugin",
24+
description="Test MCP Plugin",
25+
server_config=config,
26+
)
27+
28+
assert plugin is not None
29+
assert plugin.name == "TestMCPPlugin"
30+
assert plugin.functions.get("get_name") is not None
31+
assert plugin.functions["get_name"].parameters[0].name == "name"
32+
assert plugin.functions["get_name"].parameters[0].type_ == "string"
33+
assert plugin.functions["get_name"].parameters[0].is_required
34+
assert plugin.functions.get("set_name") is not None
35+
36+
kernel.add_plugin(plugin)
37+
38+
result = await plugin.functions["get_name"].invoke(kernel, arguments=KernelArguments(name="test"))
39+
assert "test: Test" in result.value

python/tests/unit/connectors/mcp/test_mcp.py

+44-34
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
# Copyright (c) Microsoft. All rights reserved.
2-
import os
32
from typing import TYPE_CHECKING
43
from unittest.mock import MagicMock, patch
54

65
import pytest
7-
from mcp import ClientSession, StdioServerParameters
6+
from mcp import ClientSession, ListToolsResult, StdioServerParameters, Tool
87

98
from semantic_kernel.connectors.mcp import (
109
MCPSseServerConfig,
1110
MCPStdioServerConfig,
1211
create_plugin_from_mcp_server,
1312
)
1413
from semantic_kernel.exceptions import KernelPluginInvalidConfigurationError
15-
from semantic_kernel.functions import KernelArguments
1614

1715
if TYPE_CHECKING:
18-
from semantic_kernel import Kernel
16+
pass
1917

2018

2119
async def test_mcp_server_config_session_initialize():
@@ -104,34 +102,6 @@ async def test_mcp_stdio_server_config_failed_get_session():
104102
pass
105103

106104

107-
async def test_from_mcp(kernel: "Kernel"):
108-
mcp_server_path = os.path.join(os.path.dirname(__file__), "../../../assets/test_plugins", "TestMCPPlugin")
109-
mcp_server_file = "mcp_server.py"
110-
config = MCPStdioServerConfig(
111-
command="uv",
112-
args=["--directory", mcp_server_path, "run", mcp_server_file],
113-
)
114-
115-
plugin = await create_plugin_from_mcp_server(
116-
plugin_name="TestMCPPlugin",
117-
description="Test MCP Plugin",
118-
server_config=config,
119-
)
120-
121-
assert plugin is not None
122-
assert plugin.name == "TestMCPPlugin"
123-
assert plugin.functions.get("get_name") is not None
124-
assert plugin.functions["get_name"].parameters[0].name == "name"
125-
assert plugin.functions["get_name"].parameters[0].type_ == "string"
126-
assert plugin.functions["get_name"].parameters[0].is_required
127-
assert plugin.functions.get("set_name") is not None
128-
129-
kernel.add_plugin(plugin)
130-
131-
result = await plugin.functions["get_name"].invoke(kernel, arguments=KernelArguments(name="test"))
132-
assert "test: Test" in result.value
133-
134-
135105
@patch("semantic_kernel.connectors.mcp.stdio_client")
136106
@patch("semantic_kernel.connectors.mcp.ClientSession")
137107
async def test_with_kwargs_stdio(mock_session, mock_client):
@@ -145,7 +115,21 @@ async def test_with_kwargs_stdio(mock_session, mock_client):
145115

146116
# Make the mock_stdio_client return an AsyncMock for the context manager
147117
mock_client.return_value = mock_generator
148-
await create_plugin_from_mcp_server(
118+
mock_session.return_value.__aenter__.return_value.list_tools.return_value = ListToolsResult(
119+
tools=[
120+
Tool(
121+
name="get_name",
122+
description="Get Name",
123+
inputSchema={
124+
"properties": {
125+
"name": {"type": "string"},
126+
},
127+
"required": ["name"],
128+
},
129+
)
130+
]
131+
)
132+
plugin = await create_plugin_from_mcp_server(
149133
plugin_name="TestMCPPlugin",
150134
description="Test MCP Plugin",
151135
command="uv",
@@ -154,6 +138,12 @@ async def test_with_kwargs_stdio(mock_session, mock_client):
154138
mock_client.assert_called_once_with(
155139
server=StdioServerParameters(command="uv", args=["--directory", "path", "run", "file.py"])
156140
)
141+
assert plugin is not None
142+
assert plugin.name == "TestMCPPlugin"
143+
assert plugin.description == "Test MCP Plugin"
144+
assert plugin.functions.get("get_name") is not None
145+
assert plugin.functions["get_name"].parameters[0].name == "name"
146+
assert plugin.functions["get_name"].parameters[0].is_required
157147

158148

159149
@patch("semantic_kernel.connectors.mcp.sse_client")
@@ -169,9 +159,29 @@ async def test_with_kwargs_sse(mock_session, mock_client):
169159

170160
# Make the mock_stdio_client return an AsyncMock for the context manager
171161
mock_client.return_value = mock_generator
172-
await create_plugin_from_mcp_server(
162+
mock_session.return_value.__aenter__.return_value.list_tools.return_value = ListToolsResult(
163+
tools=[
164+
Tool(
165+
name="get_name",
166+
description="Get Name",
167+
inputSchema={
168+
"properties": {
169+
"name": {"type": "string"},
170+
},
171+
"required": ["name"],
172+
},
173+
)
174+
]
175+
)
176+
plugin = await create_plugin_from_mcp_server(
173177
plugin_name="TestMCPPlugin",
174178
description="Test MCP Plugin",
175179
url="http://localhost:8080/sse",
176180
)
177181
mock_client.assert_called_once_with(url="http://localhost:8080/sse")
182+
assert plugin is not None
183+
assert plugin.name == "TestMCPPlugin"
184+
assert plugin.description == "Test MCP Plugin"
185+
assert plugin.functions.get("get_name") is not None
186+
assert plugin.functions["get_name"].parameters[0].name == "name"
187+
assert plugin.functions["get_name"].parameters[0].is_required

0 commit comments

Comments
 (0)