Title
McpTool's require_confirmation gate doesn't set skip_summarization, causing an infinite confirmation loop
Labels / component
tools / mcp_tool / tool confirmation (experimental)
Body
Environment
- google-adk 2.5.0 and 2.8.0 (both verified in source, see below)
- Python 3.12,
McpToolset(..., require_confirmation=True) over StreamableHTTPConnectionParams
- Reproduced with
adk web-launched runs and on Vertex AI Agent Engine (any runner that drives BaseLlmFlow)
Summary
When a tool served by McpToolset(require_confirmation=True) is called, McpTool.run_async's confirmation branch calls tool_context.request_confirmation(...) and returns the "This tool call requires confirmation, please approve or reject." error dict — but unlike FunctionTool.run_async, it never sets tool_context.actions.skip_summarization = True.
Because the flag is missing, the function-response event carrying the pause error is not treated as a final response, so the LLM flow re-invokes the model with that error in context. A model that reacts to the "error" by retrying the tool call produces a new confirmation request, which pauses again, and so on — an unbounded loop of tool call → pause → model re-invocation → retried tool call within a single invocation, with no user input in between. We observed ~15 consecutive createItem → adk_request_confirmation cycles in one turn before killing the session; every pause event in the trace had empty actions.
FunctionTool with require_confirmation=True does not loop: its gate sets the flag, the turn ends at the pause, and the client gets exactly one adk_request_confirmation to answer.
Source comparison
google/adk/tools/function_tool.py (2.8.0, line ~343) — flag is set:
if require_confirmation:
if not tool_context.tool_confirmation:
tool_context.request_confirmation(
hint=(...),
)
tool_context.actions.skip_summarization = True
return {
'error': 'This tool call requires confirmation, please approve or reject.'
}
google/adk/tools/mcp_tool/mcp_tool.py (2.8.0, line ~401) — flag is never set:
if require_confirmation:
if not tool_context.tool_confirmation:
tool_context.request_confirmation(
hint=(...),
)
return {
"error": "This tool call requires confirmation, please approve or reject."
}
The McpTool branch is otherwise identical to the FunctionTool one; the missing line looks like an oversight when the confirmation gate was brought to MCP tools (#3008 is the feature request that added it).
Steps to reproduce
- Any MCP server exposing one write-ish tool (a trivial FastMCP
create_thing(name) is enough).
- Wire it into an agent via
McpToolset(connection_params=..., require_confirmation=True).
- Prompt the agent to call the tool ("create a thing called banana").
- Inspect the event stream: the confirmation pause is followed by another model turn and a repeated tool call instead of ending the invocation. With a model that persistently retries on tool errors (we hit this with gemini-2.5-flash class models), the loop does not terminate on its own.
Expected behavior
The invocation ends at the confirmation pause — exactly one adk_request_confirmation long-running call is emitted, and nothing else happens until the client answers it with a FunctionResponse ({"confirmed": true|false}), the same contract FunctionTool honors.
Workaround we ship
A before_tool_callback that performs the gate itself for MCP tools, so McpTool.run_async's branch is never reached:
def pause_for_confirmation(tool, args, tool_context):
if tool.name not in GATED_TOOL_NAMES or tool_context.tool_confirmation:
return None
tool_context.request_confirmation(
hint=f"Please approve or reject the tool call {tool.name}() by responding"
" with a FunctionResponse with an expected ToolConfirmation payload.",
)
tool_context.actions.skip_summarization = True
return {"error": "This tool call requires confirmation, please approve or reject."}
This works because the flow copies the callback's tool_context.actions onto the response event, making the pause final. Happy to open a PR adding the one-line fix to McpTool if that's welcome.
Title
McpTool'srequire_confirmationgate doesn't setskip_summarization, causing an infinite confirmation loopLabels / component
tools / mcp_tool / tool confirmation (experimental)
Body
Environment
McpToolset(..., require_confirmation=True)overStreamableHTTPConnectionParamsadk web-launched runs and on Vertex AI Agent Engine (any runner that drivesBaseLlmFlow)Summary
When a tool served by
McpToolset(require_confirmation=True)is called,McpTool.run_async's confirmation branch callstool_context.request_confirmation(...)and returns the "This tool call requires confirmation, please approve or reject." error dict — but unlikeFunctionTool.run_async, it never setstool_context.actions.skip_summarization = True.Because the flag is missing, the function-response event carrying the pause error is not treated as a final response, so the LLM flow re-invokes the model with that error in context. A model that reacts to the "error" by retrying the tool call produces a new confirmation request, which pauses again, and so on — an unbounded loop of
tool call → pause → model re-invocation → retried tool callwithin a single invocation, with no user input in between. We observed ~15 consecutivecreateItem → adk_request_confirmationcycles in one turn before killing the session; every pause event in the trace had emptyactions.FunctionToolwithrequire_confirmation=Truedoes not loop: its gate sets the flag, the turn ends at the pause, and the client gets exactly oneadk_request_confirmationto answer.Source comparison
google/adk/tools/function_tool.py(2.8.0, line ~343) — flag is set:google/adk/tools/mcp_tool/mcp_tool.py(2.8.0, line ~401) — flag is never set:The
McpToolbranch is otherwise identical to theFunctionToolone; the missing line looks like an oversight when the confirmation gate was brought to MCP tools (#3008 is the feature request that added it).Steps to reproduce
create_thing(name)is enough).McpToolset(connection_params=..., require_confirmation=True).Expected behavior
The invocation ends at the confirmation pause — exactly one
adk_request_confirmationlong-running call is emitted, and nothing else happens until the client answers it with aFunctionResponse({"confirmed": true|false}), the same contractFunctionToolhonors.Workaround we ship
A
before_tool_callbackthat performs the gate itself for MCP tools, soMcpTool.run_async's branch is never reached:This works because the flow copies the callback's
tool_context.actionsonto the response event, making the pause final. Happy to open a PR adding the one-line fix toMcpToolif that's welcome.