PrefectHQ/fastmcp · error · ToolError
Tool call limit exceeded: at most {max_tool_calls} call_tool
Error message
Tool call limit exceeded: at most {max_tool_calls} call_tool() invocations are allowed per execute(). What it means
The CodeMode execute tool wraps sandboxed code's call_tool() with a per-execute budget (max_tool_calls). Each invocation increments a counter; once it exceeds the limit a ToolError is raised, protecting the server from runaway generated code.
Source
Thrown at fastmcp_slim/fastmcp/experimental/transforms/code_mode.py:613
str,
Field(
description=(
"Python async code to execute tool calls via call_tool(name, arguments)"
)
),
],
ctx: Context = None, # type: ignore[assignment] # ty:ignore[invalid-parameter-default]
) -> Any:
"""Execute tool calls using Python code."""
call_count = 0
async def call_tool(tool_name: str, params: dict[str, Any]) -> Any:
nonlocal call_count
if max_tool_calls is not None:
call_count += 1
if call_count > max_tool_calls:
raise ToolError(
f"Tool call limit exceeded: at most {max_tool_calls} "
"call_tool() invocations are allowed per execute()."
)
backend_tools = await transform.get_tool_catalog(ctx)
tool = transform._find_tool(tool_name, backend_tools)
if tool is None:
raise NotFoundError(f"Unknown tool: {tool_name}")
result = await ctx.fastmcp.call_tool(tool.name, params)
return _unwrap_tool_result(result)
return await transform.sandbox_provider.run(
code,
external_functions={"call_tool": call_tool},
)
return Tool.from_function(View on GitHub (pinned to 1f02114297)
Solutions
- Raise max_tool_calls when constructing CodeModeTransform to accommodate legitimate workloads
- Set max_tool_calls=None to disable the limit (only if generated code is trusted)
- Refactor the calling code to batch work into fewer tool calls
- Handle ToolError in the caller and instruct the model to reduce tool usage
Example fix
// before transform = CodeModeTransform(max_tool_calls=5) # loop calls tool 50x // after transform = CodeModeTransform(max_tool_calls=100) # or batch the loop into one call
Defensive patterns
Strategy: try-catch
Try / catch
try:
result = await transform.execute(code)
except ToolError as e:
if "Tool call limit exceeded" in str(e):
# raise max_tool_calls or reduce tool usage in generated code
...
raise Prevention
- Size max_tool_calls to your workload, or set it to None for trusted code
- Prompt the model to batch tool operations instead of per-item calls
- Instrument generated code to count call_tool() invocations before hitting the cap
When it happens
Trigger: Generated code inside the sandbox calls call_tool() more than max_tool_calls times during a single execute() — e.g. unbounded loops iterating a tool per item, or retry logic that keeps calling tools.
Common situations: LLM-generated code with while/for loops calling tools repeatedly; processing large datasets one call at a time; misconfigured (too low) max_tool_calls for legitimate workloads.
Related errors
- Unknown tool: {tool_name}
- CodeMode requires pydantic-monty for the Monty sandbox provi
- Discovery tool name '{self.execute_tool_name}' collides with
- Discovery tools must have unique names.
- mcp_tool() got unexpected keyword argument(s): {sorted(unkno
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/9abea603bed8ce97.
Report an issue: GitHub.