From a5b09195002cf6978a4bcc9546d01f8360788010 Mon Sep 17 00:00:00 2001 From: ssccinng Date: Sun, 25 Oct 2026 15:14:47 +0800 Subject: [PATCH] Keep optional MCP tools available --- python/packages/core/agent_framework/_mcp.py | 4 +- python/packages/core/tests/core/test_mcp.py | 43 ++++++++++++++------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index c4f93237f03..351adbc1e2f 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -3107,9 +3107,9 @@ async def _load_tools_locked(self) -> None: for tool in tool_list.tools: task_support = getattr(getattr(tool, "execution", None), "task_support", None) - if task_support in ["required", "optional"]: + if task_support == "required": logger.warning( - "Skipping MCP tool %r because it (might) require the Tasks extension, " + "Skipping MCP tool %r because it requires the Tasks extension, " "which MCP Python SDK 2.2 does not implement.", tool.name, ) diff --git a/python/packages/core/tests/core/test_mcp.py b/python/packages/core/tests/core/test_mcp.py index 757e2a15f88..4e58dd0f618 100644 --- a/python/packages/core/tests/core/test_mcp.py +++ b/python/packages/core/tests/core/test_mcp.py @@ -558,7 +558,7 @@ async def list_tools(params: types.PaginatedRequestParams | None = None) -> type assert tool._tool_param_names_by_name == ({} if empty_snapshot else {"keep": {"query"}}) -async def test_load_tools_skips_task_capable_tools(caplog: pytest.LogCaptureFixture) -> None: +async def test_load_tools_skips_only_required_task_capable_tools(caplog: pytest.LogCaptureFixture) -> None: tool = MCPTool(name="docs") # type: ignore[abstract] # ty: ignore[call-non-callable] mock_session = AsyncMock() tool.session = mock_session @@ -573,7 +573,7 @@ async def test_load_tools_skips_task_capable_tools(caplog: pytest.LogCaptureFixt ), types.Tool( name="optional", - input_schema={"type": "object", "properties": {}}, + input_schema={"type": "object", "properties": {"query": {"type": "string"}}}, execution=types.ToolExecution(task_support="optional"), _meta={"mode": "optional"}, ), @@ -593,19 +593,38 @@ async def test_load_tools_skips_task_capable_tools(caplog: pytest.LogCaptureFixt with caplog.at_level(logging.WARNING, logger=logger.name): await tool.load_tools() - assert [function.name for function in tool.functions] == ["forbidden", "ordinary"] - assert tool._tool_call_meta_by_name == {} - assert set(tool._tool_param_names_by_name) == {"forbidden", "ordinary"} - for tool_name in ("required", "optional"): - assert ( - f"Skipping MCP tool '{tool_name}' because it (might) require the Tasks extension, " - "which MCP Python SDK 2.2 does not implement." - ) in caplog.messages + assert [function.name for function in tool.functions] == ["optional", "forbidden", "ordinary"] + assert tool._tool_call_meta_by_name == {"optional": {"mode": "optional"}} + assert tool._tool_param_names_by_name == { + "optional": {"query"}, + "forbidden": set(), + "ordinary": set(), + } + assert "required" not in tool._tool_call_meta_by_name + assert "required" not in tool._tool_param_names_by_name + assert caplog.messages == [ + "Skipping MCP tool 'required' because it requires the Tasks extension, " + "which MCP Python SDK 2.2 does not implement." + ] - for function in tool.functions: + optional_function = tool.functions[0] + optional_result = await optional_function.invoke(arguments={"query": "search term"}) + assert _mcp_result_to_text(optional_result) == "ok" + optional_call = mock_session.call_tool.await_args + assert optional_call is not None + assert optional_call.args == ("optional",) + assert set(optional_call.kwargs) == {"arguments", "meta"} + assert optional_call.kwargs["arguments"] == {"query": "search term"} + assert optional_call.kwargs["meta"]["mode"] == "optional" + + for function in tool.functions[1:]: await function.invoke(arguments={}) - assert [call.args[0] for call in mock_session.call_tool.await_args_list] == ["forbidden", "ordinary"] + assert [call.args[0] for call in mock_session.call_tool.await_args_list] == [ + "optional", + "forbidden", + "ordinary", + ] async def test_tool_refresh_preserves_prompt_when_server_advertises_same_raw_name() -> None: