Skip to content

Commit ea0406e

Browse files
Python: Enforce excluded_functions on MCP tool invocation path
Ensure functions excluded via excluded_functions are consistently handled on both the list_tools and call_tool paths of the MCP server, returning a method-not-found error for tools that are not exposed. Also removes a leftover debug print statement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent dfc5227 commit ea0406e

2 files changed

Lines changed: 60 additions & 2 deletions

File tree

python/semantic_kernel/connectors/mcp.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,7 @@ def create_mcp_server_from_kernel(
10231023
functions_to_expose = [
10241024
func for func in kernel.get_full_list_of_function_metadata() if func.name not in (excluded_functions or [])
10251025
]
1026+
exposed_names = frozenset(func.name for func in functions_to_expose)
10261027

10271028
if len(functions_to_expose) > 0:
10281029

@@ -1058,8 +1059,15 @@ async def _call_tool(
10581059
*args: Any,
10591060
) -> Sequence[types.TextContent | types.ImageContent | types.AudioContent | types.EmbeddedResource]:
10601061
"""Call a tool in the kernel."""
1061-
await _log(level="debug", data=f"Calling tool with args: {args}")
10621062
function_name, arguments = args[0], args[1]
1063+
if function_name not in exposed_names:
1064+
raise McpError(
1065+
types.ErrorData(
1066+
code=types.METHOD_NOT_FOUND,
1067+
message=f"Unknown tool: {function_name}",
1068+
)
1069+
)
1070+
await _log(level="debug", data=f"Calling tool: {function_name}")
10631071
result = await _call_kernel_function(function_name, arguments)
10641072
if result:
10651073
value = result.value
@@ -1165,7 +1173,6 @@ async def _set_logging_level(level: types.LoggingLevel) -> None:
11651173
async def _call_kernel_function(function_name: str, arguments: Any) -> FunctionResult | None:
11661174
function = kernel.get_function(plugin_name=None, function_name=function_name)
11671175
arguments["server"] = server
1168-
print("arguments", arguments)
11691176
return await function.invoke(kernel=kernel, **arguments)
11701177

11711178
return server

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)