What happened?
A model configured with an MCP tool reference using require_approval: "never" (so the proxy auto-executes it server-side) is deployed behind an agentic client that sends its own tools in the same request, such as Claude Code sending Read/Bash/Edit. The auto-execute loop takes over the entire tool_use turn and tries to execute every tool_use block the model emits, including the client's own non-MCP tools. Those tools are not in the MCP server map, so the proxy raises a KeyError that gets swallowed into Error executing tool: '<tool_name>' and fed back to the model. The model retries, fails again, and eventually gives up. From the client's perspective all of its native tools (Read, Bash, etc.) are broken for any request routed to that model
What I expected: when require_approval: "never" is set, the proxy should auto-execute only the MCP tools it injected. A tool_use whose name does not resolve to a known MCP server should be passed through to the client unchanged so the client can execute it locally, the way a normal tool_use turn works when no MCP tool is present
User Flow
Before a fix
- A user creates a virtual key and a team in the LiteLLM proxy UI, then registers an HTTP MCP server (say
image_understand) and grants the team access to it
- The user attaches the MCP tool to a model via the model's LiteLLM Params, e.g.
{"tools": [{"type": "mcp", "server_url": "litellm_proxy/mcp/image_understand", "require_approval": "never"}]}
- The user points an agentic client (Claude Code) at
POST /v1/messages on the proxy, using that model and that virtual key. The client sends its own tools (Read, Bash, Edit, ...) alongside whatever the proxy injects
- The model decides to call Read on a local file. The proxy's auto-execute loop intercepts that tool_use, fails with
Error executing tool: 'Read', and returns the error text to the model instead of handing the tool_use back to the client
- The model retries with Bash, gets
Error executing tool: 'Bash', and eventually stops with a message saying the tools are unavailable. The user sees tool call failures and the client's native tooling is dead for that model
After a fix
- Same setup as above: MCP server registered, team granted access, MCP tool attached to the model with
require_approval: "never"
- The user points Claude Code at
POST /v1/messages with that model and key, sending its own tools
- The model calls Read on a local file. The proxy recognizes Read is not one of its MCP tools, returns the tool_use block to the client in the response, and lets the client execute it locally
- When the model calls the injected MCP tool (
image_understand), the proxy auto-executes it server-side as before and feeds the result back
- Both the client's native tools and the injected MCP tool work in the same turn
Proof the bug occurs
I verified the root cause by reading the current main branch (v1.98.0-dev.2). I was not able to capture a full Claude Code trace against my own proxy in time for this report, so the proof below is the code path plus the user-visible symptom. I can add a full end-to-end curl reproduction if the maintainers want one
Config / setup: a model with an MCP tool attached via LiteLLM Params
{"tools": [{"type": "mcp", "server_url": "litellm_proxy/mcp/image_understand", "require_approval": "never"}]}
Version or commit: v1.98.0-dev.2 on main
Commands and their full output
The auto-execute loop in litellm/llms/anthropic/experimental_pass_through/messages/mcp_handler.py extracts every tool_use block without checking whether it belongs to an MCP server
# line 38-40
def _extract_tool_use_blocks(response):
return tuple(block for block in _get_response_content(response) if block.get("type") == "tool_use")
# line 139-158 (abridged)
for _ in range(MAX_MCP_TOOL_USE_ITERATIONS):
if _get_stop_reason(response) != "tool_use":
break
tool_use_blocks = _extract_tool_use_blocks(response)
if not tool_use_blocks:
break
tool_results = await LiteLLM_Proxy_MCP_Handler._execute_tool_calls(
tool_server_map=tool_server_map,
tool_calls=list(tool_use_blocks),
...
)
tool_server_map is built only from the injected MCP tools (in _process_mcp_tools_without_openai_transform). When _execute_tool_calls looks up a client-side tool name that is not in that map, it throws KeyError
# litellm/responses/mcp/litellm_proxy_mcp_handler.py line 675
server_name = tool_server_map[tool_name]
That KeyError is caught by the broad except at line 891-905 and turned into the user-visible error string
# line 891-905 (abridged)
except Exception as e:
...
tool_results.append({
"tool_call_id": tool_call_id,
"result": f"Error executing tool: {e}",
"name": tool_name,
})
So a model emitting tool_use for Read produces Error executing tool: 'Read', which matches what I observed in the proxy spend logs and what Claude Code surfaced as "tool call failed"
The symptom from the agentic client side (Claude Code talking to the proxy over /v1/messages): the model attempts Read on a file that exists, gets Error executing tool: 'Read' back, tries Bash, gets Error executing tool: 'Bash', then tells the user it cannot read files or run commands. A plain curl client that sends no tools of its own works perfectly against the same model, because then every tool_use resolves to an MCP tool. This is why the bug is specific to agentic clients that bring their own tools
What part of LiteLLM is this about?
Proxy
What LiteLLM version are you on ?
v1.98.0-dev.2
Twitter / LinkedIn details
No response
What happened?
A model configured with an MCP tool reference using
require_approval: "never"(so the proxy auto-executes it server-side) is deployed behind an agentic client that sends its own tools in the same request, such as Claude Code sending Read/Bash/Edit. The auto-execute loop takes over the entire tool_use turn and tries to execute every tool_use block the model emits, including the client's own non-MCP tools. Those tools are not in the MCP server map, so the proxy raises a KeyError that gets swallowed intoError executing tool: '<tool_name>'and fed back to the model. The model retries, fails again, and eventually gives up. From the client's perspective all of its native tools (Read, Bash, etc.) are broken for any request routed to that modelWhat I expected: when
require_approval: "never"is set, the proxy should auto-execute only the MCP tools it injected. A tool_use whose name does not resolve to a known MCP server should be passed through to the client unchanged so the client can execute it locally, the way a normal tool_use turn works when no MCP tool is presentUser Flow
Before a fix
image_understand) and grants the team access to it{"tools": [{"type": "mcp", "server_url": "litellm_proxy/mcp/image_understand", "require_approval": "never"}]}POST /v1/messageson the proxy, using that model and that virtual key. The client sends its own tools (Read, Bash, Edit, ...) alongside whatever the proxy injectsError executing tool: 'Read', and returns the error text to the model instead of handing the tool_use back to the clientError executing tool: 'Bash', and eventually stops with a message saying the tools are unavailable. The user sees tool call failures and the client's native tooling is dead for that modelAfter a fix
require_approval: "never"POST /v1/messageswith that model and key, sending its own toolsimage_understand), the proxy auto-executes it server-side as before and feeds the result backProof the bug occurs
I verified the root cause by reading the current main branch (v1.98.0-dev.2). I was not able to capture a full Claude Code trace against my own proxy in time for this report, so the proof below is the code path plus the user-visible symptom. I can add a full end-to-end curl reproduction if the maintainers want one
Config / setup: a model with an MCP tool attached via LiteLLM Params
{"tools": [{"type": "mcp", "server_url": "litellm_proxy/mcp/image_understand", "require_approval": "never"}]}Version or commit: v1.98.0-dev.2 on main
Commands and their full output
The auto-execute loop in
litellm/llms/anthropic/experimental_pass_through/messages/mcp_handler.pyextracts every tool_use block without checking whether it belongs to an MCP servertool_server_mapis built only from the injected MCP tools (in_process_mcp_tools_without_openai_transform). When_execute_tool_callslooks up a client-side tool name that is not in that map, it throws KeyErrorThat KeyError is caught by the broad except at line 891-905 and turned into the user-visible error string
So a model emitting
tool_useforReadproducesError executing tool: 'Read', which matches what I observed in the proxy spend logs and what Claude Code surfaced as "tool call failed"The symptom from the agentic client side (Claude Code talking to the proxy over
/v1/messages): the model attempts Read on a file that exists, getsError executing tool: 'Read'back, tries Bash, getsError executing tool: 'Bash', then tells the user it cannot read files or run commands. A plain curl client that sends no tools of its own works perfectly against the same model, because then every tool_use resolves to an MCP tool. This is why the bug is specific to agentic clients that bring their own toolsWhat part of LiteLLM is this about?
Proxy
What LiteLLM version are you on ?
v1.98.0-dev.2
Twitter / LinkedIn details
No response