feat(mcp): add nonblocking_tools to MCPToolset#5694
Conversation
MCPToolset now extends AsyncToolset and gains an `async_mode=True` flag. When set, MCP tools are built with an `AsyncRunContext` parameter that forwards MCP progress notifications via `ctx.update`, so the agent can narrate progress while a slow MCP tool runs in the background instead of blocking the reply loop. `AsyncToolset` no longer stores `get_running_tasks` / `cancel_task` in `_tools`; they're exposed through a `tools` property override that subclasses can condition. `MCPToolset` overrides it to expose the helpers only when `async_mode=True`, so the legacy MCP UX is unchanged when the flag is off. `MCPServer.list_tools` now caches per `async_mode`, and the example MCP server gains a long-running `book_flight` tool that emits progress updates.
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
theomonnom
left a comment
There was a problem hiding this comment.
lgtm, let's have async_mode enabled by default?
maybe the arguments becomes something like nonblocking_tools: list[str]?
Public API on MCPToolset becomes `nonblocking_tools: bool | list[str]` defaulting to `True`, so MCP tools run non-blocking by default and callers can opt specific tools in or out by name. Empty list / `False` falls back to the legacy blocking behavior. MCPServer.list_tools routes the per-tool decision down to _make_function_tool, which still takes a bool, and caches the resolved bool|frozenset key.
|
Let's wait for #5711 |
| *, | ||
| id: str, | ||
| mcp_server: MCPServer, | ||
| nonblocking_tools: bool | list[str] = True, |
There was a problem hiding this comment.
π‘ Legacy mcp_servers path silently gets non-blocking behavior and extra LLM-visible tools
The deprecated mcp_servers path in agent_activity.py:700 creates MCPToolset with the default nonblocking_tools=True. Before this PR, MCPToolset extended Toolset (purely blocking). Now it extends AsyncToolset, so all MCP tools from the legacy path become non-blocking by default. This means: (1) get_running_tasks and cancel_task tools are exposed to the LLM, (2) a _lk_agents_confirm_duplicate parameter is injected into every tool schema, and (3) duplicate-call detection (mode "confirm") is active β which could reject legitimate repeated calls. For tools without MCP progress notifications the end result is functionally similar (the wrapper blocks on _pending_fut until the tool completes), but the schema changes and extra exposed tools could confuse the LLM or subtly change behavior for users on the deprecated API.
Prompt for agents
The MCPToolset constructor defaults nonblocking_tools=True (line 523 of mcp.py). This is fine for new callers who opt in explicitly, but the legacy deprecated path in agent_activity.py:700 creates MCPToolset without specifying nonblocking_tools, inheriting the True default. This changes behavior for all existing users of the deprecated mcp_servers parameter.
Consider either:
1. Changing the default to False so the legacy path keeps its original blocking behavior, or
2. Explicitly passing nonblocking_tools=False at the legacy creation site in agent_activity.py:700
Was this helpful? React with π or π to provide feedback.
Summary
MCPToolsetnow extendsAsyncToolsetand gains a keyword-onlyasync_modeflag. When enabled, MCP tools are built with anAsyncRunContextparameter and the MCP server's progress notifications are forwarded viactx.update, so the agent can narrate progress while a slow MCP tool runs in the background.Example
examples/voice_agents/mcp/server.pynow ships a fastget_weather(sync, no progress) and a long-runningbook_flightthat emitsctx.report_progressupdates over ~70s.mcp-agent.pyusesasync_mode=Trueand bumpsclient_session_timeout_seconds=120since the per-request timeout otherwise fires beforebook_flightreturns.