diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index c4f93237f03..a9686c9ab1b 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -3107,9 +3107,11 @@ 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"]: + # Only "required" tools need the Tasks extension. Per the MCP spec, a tool with + # taskSupport "optional" also accepts a plain tools/call, which is what we send. + 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..a5808315c6e 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_task_required_tools(caplog: pytest.LogCaptureFixture) -> None: tool = MCPTool(name="docs") # type: ignore[abstract] # ty: ignore[call-non-callable] mock_session = AsyncMock() tool.session = mock_session @@ -593,19 +593,21 @@ 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, " + assert [function.name for function in tool.functions] == ["optional", "forbidden", "ordinary"] + assert tool._tool_call_meta_by_name == {"optional": {"mode": "optional"}} + assert set(tool._tool_param_names_by_name) == {"optional", "forbidden", "ordinary"} + skip_messages = [message for message in caplog.messages if message.startswith("Skipping MCP tool")] + assert skip_messages == [ + ( + "Skipping MCP tool 'required' because it requires the Tasks extension, " "which MCP Python SDK 2.2 does not implement." - ) in caplog.messages + ) + ] for function in tool.functions: 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: