---
title: "Code Mode"
sidebarTitle: "Code Mode"
description: "AI writes Python to orchestrate tools. Reduces input token usage by up to 92.8% when using multiple MCP servers."
icon: "code"
---
This feature is only available on `v1.4.0-prerelease1` and above.
## Overview
**Code Mode** is a transformative approach to using MCP that solves a critical problem at scale:
> **The Problem:** When you connect 8-10 MCP servers (150+ tools), every single request includes all tool definitions in the context. The LLM spends most of its budget reading tool catalogs instead of doing actual work.
**The Solution:** Instead of exposing 150 tools directly, Code Mode exposes just **four generic tools**. The LLM uses those tools to write Python code (Starlark) that orchestrates everything else in a sandbox.
### The Impact
Compare a workflow across 5 MCP servers with ~100 tools:
**Classic MCP Flow:**
- 6 LLM turns
- 100 tools in context **every turn** (600 tool-definition tokens)
- All intermediate results flow through the model
**Code Mode Flow:**
- 3-4 LLM turns
- Only 4 tools + definitions on-demand
- Intermediate results processed in sandbox
**Result: Up to 92.8% fewer input tokens, 92.2% lower estimated cost, and around 40% faster execution in large MCP deployments.**
### Benchmark Results
Bifrost Code Mode was benchmarked against classic MCP across three controlled rounds with increasing MCP footprint. Each round used the same query set with Code Mode off and on.
| Round | MCP footprint | Pass rate, classic MCP | Pass rate, Code Mode | Input tokens, classic MCP | Input tokens, Code Mode | Input token change | Est. cost, classic MCP | Est. cost, Code Mode | Cost change |
|-------|---------------|------------------------|----------------------|---------------------------|-------------------------|--------------------|------------------------|----------------------|-------------|
| 1 | 96 tools / 6 servers | 64/64 (100%) | 64/64 (100%) | 19.9M | 8.3M | -58.2% | \$104.04 | \$46.06 | -55.7% |
| 2 | 251 tools / 11 servers | 64/65 (98.5%) | 65/65 (100%) | 35.7M | 5.5M | -84.5% | \$180.07 | \$29.80 | -83.4% |
| 3 | 508 tools / 16 servers | 65/65 (100%) | 65/65 (100%) | 75.1M | 5.4M | -92.8% | \$377.00 | \$29.00 | -92.2% |
At around 500 tools, Code Mode reduced average input tokens per query by roughly **14x**: from **1.15M** tokens to **83K** tokens. See the [Bifrost MCP Gateway benchmark writeup](https://www.getmaxim.ai/bifrost/blog/bifrost-mcp-gateway-access-control-cost-governance-and-92-lower-token-costs-at-scale), or explore the [complete benchmark report](https://github.com/maximhq/bifrost-benchmarking/blob/main/mcp-code-mode-benchmark/benchmark_report.md).
Code Mode provides four meta-tools to the AI:
1. **`listToolFiles`** - Discover available MCP servers
2. **`readToolFile`** - Load Python stub signatures on-demand
3. **`getToolDocs`** - Get detailed documentation for a specific tool
4. **`executeToolCode`** - Execute Python code with full tool bindings
## When to Use Code Mode
**Enable Code Mode if you have:**
- ✅ 3+ MCP servers connected
- ✅ Complex multi-step workflows
- ✅ Concerned about token costs or latency
- ✅ Tools that need to interact with each other
**Keep Classic MCP if you have:**
- ✅ Only 1-2 small MCP servers
- ✅ Simple, direct tool calls
- ✅ Very latency-sensitive use cases (though Code Mode is usually faster)
**You can mix both:** Enable Code Mode for "heavy" servers (web, documents, databases) and keep small utilities as direct tools.
---
## How Code Mode Works
### The Four Tools
Instead of seeing 150+ tool definitions, the model sees four generic tools:
```mermaid
graph LR
LLM["LLM Context
Compact & Efficient"]
List["listToolFiles
Discover servers"]
Read["readToolFile
Load signatures"]
Docs["getToolDocs
Get detailed docs"]
Execute["executeToolCode
Run code with bindings"]
Hidden["All other MCP servers
hidden behind these 4 tools"]
LLM --> List
LLM --> Read
LLM --> Docs
LLM --> Execute
List -.-> Hidden
Read -.-> Hidden
Docs -.-> Hidden
Execute -.-> Hidden
style LLM fill:#E3F2FD,stroke:#0D47A1,stroke-width:2.5px,color:#1A1A1A
style List fill:#E8F5E9,stroke:#1B5E20,stroke-width:2.5px,color:#1A1A1A
style Read fill:#FFF3E0,stroke:#BF360C,stroke-width:2.5px,color:#1A1A1A
style Docs fill:#E1F5FE,stroke:#0288D1,stroke-width:2.5px,color:#1A1A1A
style Execute fill:#F3E5F5,stroke:#4A148C,stroke-width:2.5px,color:#1A1A1A
style Hidden fill:#EEEEEE,stroke:#424242,stroke-width:1.5px,stroke-dasharray: 5 5,color:#1A1A1A
```
### The Execution Flow
```mermaid
graph LR
User["1. User Request
Search YouTube
& save to file"]
Discover["2. Discover Tools
listToolFiles()"]
GetDefs["3. Load Definitions
readToolFile()"]
Write["4. Write Code
Python
in sandbox"]
Execute["5. Execute
Real MCP calls
contained in VM"]
Result["6. Compact Result
{saved:10}"]
Response["7. Final Response
Found & saved
10 videos"]
User --> Discover
Discover --> GetDefs
GetDefs --> Write
Write --> Execute
Execute --> Result
Result --> Response
style User fill:#E3F2FD,stroke:#0D47A1,stroke-width:2.5px,color:#1A1A1A
style Discover fill:#F3E5F5,stroke:#4A148C,stroke-width:2.5px,color:#1A1A1A
style GetDefs fill:#F3E5F5,stroke:#4A148C,stroke-width:2.5px,color:#1A1A1A
style Write fill:#FFF3E0,stroke:#BF360C,stroke-width:2.5px,color:#1A1A1A
style Execute fill:#E8F5E9,stroke:#1B5E20,stroke-width:3px,color:#1A1A1A
style Result fill:#FFFDE7,stroke:#F57F17,stroke-width:2.5px,color:#1A1A1A
style Response fill:#E8F5E9,stroke:#1B5E20,stroke-width:2.5px,color:#1A1A1A
```
**Key insight:** All the complex orchestration happens inside the sandbox. The LLM only receives the final, compact result, not every intermediate step.
---
## Why This Matters at Scale
Take a multi-step workflow such as looking up a customer, checking their order history, applying a discount, and sending a confirmation.
### Classic MCP: every turn carries the full tool list
With classic MCP, every intermediate result returns to the model, and every next turn includes the complete set of available tool definitions again. As the number of connected MCP servers grows, the model keeps paying to reread the same tool catalog.
### Code Mode: discover, write once, execute once
With Code Mode, the model discovers the relevant stubs, writes a short orchestration script, and Bifrost runs the tool calls inside the Starlark sandbox. The intermediate tool results stay inside the sandbox, and the model receives the compact final output.
This is why the savings grow with scale: classic MCP cost grows with every connected tool, while Code Mode cost is bounded by the files and documentation the model actually reads. In the benchmark rounds, this produced 3-4x fewer LLM round trips and input-token savings from **58.2%** to **92.8%** as tool count increased.
---
## Enabling Code Mode
Code Mode must be enabled **per MCP client**. Once enabled, that client's tools are accessed through the four meta-tools rather than exposed directly.
**Best practice:** Enable Code Mode for 3+ servers or any "heavy" server (web search, documents, databases).
### Enable Code Mode for a Client
1. Navigate to **MCP Gateway** in the sidebar
2. Click on a client row to open the configuration sheet
3. In the **Basic Information** section, toggle **Code Mode Server** to enabled
4. Click **Save Changes**
Once enabled:
- This client's tools are no longer in the default tool list
- They become accessible through `listToolFiles()` and `readToolFile()`
- The AI can write code using `executeToolCode()` to call them
```bash
# When adding a new client
curl -X POST http://localhost:8080/api/mcp/client \
-H "Content-Type: application/json" \
-d '{
"name": "youtube",
"connection_type": "http",
"connection_string": "http://localhost:3001/mcp",
"tools_to_execute": ["*"],
"is_code_mode_client": true
}'
# Or update an existing client
curl -X PUT http://localhost:8080/api/mcp/client/{id} \
-H "Content-Type: application/json" \
-d '{
"name": "youtube",
"connection_type": "http",
"connection_string": "http://localhost:3001/mcp",
"tools_to_execute": ["*"],
"is_code_mode_client": true
}'
```
```json
{
"mcp": {
"client_configs": [
{
"name": "youtube",
"connection_type": "http",
"connection_string": "http://localhost:3001/mcp",
"tools_to_execute": ["*"],
"is_code_mode_client": true
},
{
"name": "filesystem",
"connection_type": "stdio",
"stdio_config": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-filesystem"]
},
"tools_to_execute": ["*"],
"is_code_mode_client": true
}
]
}
}
```
### Go SDK Setup
```go
mcpConfig := &schemas.MCPConfig{
ClientConfigs: []schemas.MCPClientConfig{
{
Name: "youtube",
ConnectionType: schemas.MCPConnectionTypeHTTP,
ConnectionString: bifrost.Ptr("http://localhost:3001/mcp"),
ToolsToExecute: []string{"*"},
IsCodeModeClient: true, // Enable code mode
},
{
Name: "filesystem",
ConnectionType: schemas.MCPConnectionTypeSTDIO,
StdioConfig: &schemas.MCPStdioConfig{
Command: "npx",
Args: []string{"-y", "@anthropic/mcp-filesystem"},
},
ToolsToExecute: []string{"*"},
IsCodeModeClient: true, // Enable code mode
},
},
}
```
---
## The Four Code Mode Tools
When Code Mode clients are connected, Bifrost automatically adds four meta-tools to every request:
When the request carries a virtual key, `listToolFiles`, `readToolFile`, and `getToolDocs` only show the tools that key is allowed to execute (its MCP `tools_to_execute` grant), on every route including `POST /v1/mcp/tool/execute`. A tool the key does not grant does not appear in discovery; grant it on the key to make it visible. Requests without a virtual key see every code mode tool, as before.
### 1. listToolFiles
Lists all available virtual `.pyi` stub files for connected code mode servers.
**Example output (Server-level binding):**
```
servers/
youtube.pyi
filesystem.pyi
```
**Example output (Tool-level binding):**
```
servers/
youtube/
search.pyi
get_video.pyi
filesystem/
read_file.pyi
write_file.pyi
```
### 2. readToolFile
Reads a virtual `.pyi` file to get compact Python function signatures for tools.
**Parameters:**
- `fileName` (required): Path like `servers/youtube.pyi` or `servers/youtube/search.pyi`
- `startLine` (optional): 1-based starting line for partial reads
- `endLine` (optional): 1-based ending line for partial reads
**Example output:**
```python
# youtube server tools
# Usage: youtube.tool_name(param=value)
# Results: the decoded JSON when the server sends JSON (a dict or list, or a number, bool or None), otherwise a str.
# json.decode(s) parses a str that holds JSON; plain text raises.
# A call that fails returns an error value: check hasattr(r, "error"); r.error holds the message, and fail(r.error) stops the run.
# For detailed docs: use getToolDocs(server="youtube", tool="tool_name")
#
# ---- server instructions (from the upstream handshake) ----
# Prefer search for discovery; call get_video only with an id search returned.
# ---- end server instructions ----
def search(query: str, maxResults: int = None) -> dict: # Search for videos
def get_video(id: str) -> dict: # Get video details
```
If the server described how its tools should be used when it connected, that guidance appears
above the signatures, so the model reads it right before writing the call. Servers that said
nothing contribute no block. `listToolFiles` names which servers have guidance, and `getToolDocs`
repeats it alongside the full tool documentation. Long guidance is trimmed to the server's
instruction limit.
This is the only place a code mode server's guidance appears. It is deliberately left out of the
aggregate that non-code-mode servers contribute to (see
[Server Instructions](/mcp/overview#server-instructions)), which every request carries: in code
mode that would describe tools the model was not given, and would put back a per-server prompt
cost that code mode exists to remove.
### 3. getToolDocs
Get detailed documentation for a specific tool when the compact signature from `readToolFile` is not sufficient.
**Parameters:**
- `server` (required): The server name (e.g., `"youtube"`)
- `tool` (required): The tool name (e.g., `"search"`)
**Example output:**
```python
# ============================================================================
# Documentation for youtube.search tool
# ============================================================================
#
# USAGE INSTRUCTIONS:
# Call tools using: result = youtube.tool_name(param=value)
# No async/await needed - calls are synchronous.
#
# CRITICAL - HANDLING RESPONSES:
# A tool returns the decoded JSON when the server sends JSON: a dict or list,
# or a number, bool or None. Any other text arrives as a str.
# 1. Check the shape, not the whole value: print(type(r)), and print(sorted(r.keys())) for a dict.
# Printing a whole response puts all of it into your context.
# 2. dict: r["key"] or r.get("key", default), never r.key. list: r[0].
# 3. str holding JSON, often a field: data = json.decode(r["body"]); plain text raises.
#
# FAILED CALLS:
# A call that fails returns an error value, not a result: hasattr(r, "error") is True and
# r.error holds the message. Call fail(r.error) to stop the run, or skip it and carry on.
#
# Common errors: key "x" not in dict, dict has no .x field or method, string index
# Fix: check type(r) first, then index the shape you actually got
# ============================================================================
def search(query: str, maxResults: int = None) -> dict:
"""
Search for videos on YouTube.
Args:
query (str): Search query (required)
maxResults (int): Max results to return (optional)
Returns:
The decoded JSON (usually a dict or list; a number, bool or None for a
bare value), or a str when the server's text is not JSON.
Check type(result) before indexing it.
Example:
result = youtube.search(query="...")
print(type(result)) # check the shape before indexing it
value = result.get("key") if type(result) == "dict" else result
"""
...
```
### 4. executeToolCode
Executes Python code in a sandboxed Starlark interpreter with access to all code mode server tools.
**Parameters:**
- `code` (required): Python code to execute
**Execution Environment:**
- Python code runs in a Starlark interpreter (Python subset)
- All code mode servers are exposed as global objects (e.g., `youtube`, `filesystem`)
- Tool calls are **synchronous** - no async/await needed
- Use `print()` for logging (output captured in logs)
- Assign to `result` variable to return a value
- The model receives only what the script prints and the `result` value, never the raw tool responses (a nested call that failed is noted in one line)
- Tool execution timeout applies (default 30s)
**Syntax notes:**
- Use keyword arguments: `server.tool(param="value")` NOT `server.tool({"param": "value"})`
- Access dict values with brackets: `result["key"]` NOT `result.key`
- List comprehensions work: `[x for x in items if x["active"]]`
**Example code:**
```python
# Search YouTube and return formatted results
results = youtube.search(query="AI news", maxResults=5)
titles = [item["snippet"]["title"] for item in results["items"]]
print("Found", len(titles), "videos")
result = {"titles": titles, "count": len(titles)}
```
---
## Binding Levels
Code Mode supports two binding levels that control how tools are organized in the virtual file system:
### Server-Level Binding (Default)
All tools from a server are grouped into a single `.pyi` file.
```
servers/
youtube.pyi ← Contains all youtube tools
filesystem.pyi ← Contains all filesystem tools
```
**Best for:**
- Servers with few tools
- When you want to see all tools at once
- Simpler discovery workflow
### Tool-Level Binding
Each tool gets its own `.pyi` file.
```
servers/
youtube/
search.pyi
get_video.pyi
get_channel.pyi
filesystem/
read_file.pyi
write_file.pyi
list_directory.pyi
```
**Best for:**
- Servers with many tools
- When tools have large/complex schemas
- More focused documentation per tool
### Configuring Binding Level
Binding level is a **global setting** that controls how Code Mode's virtual file system is organized. It affects how the AI discovers and loads tool definitions.
Binding level can be viewed in the MCP configuration overview:
- **Server-level (default)**: One `.pyi` file per MCP server
- Use when: 5-20 tools per server, want simple discovery
- Example: `servers/youtube.pyi` contains all YouTube tools
- **Tool-level**: One `.pyi` file per individual tool
- Use when: 30+ tools per server, want minimal context bloat
- Example: `servers/youtube/search.pyi`, `servers/youtube/list_channels.pyi`
Both modes use the same four-tool interface (`listToolFiles`, `readToolFile`, `getToolDocs`, `executeToolCode`). The choice is purely about **context efficiency per read operation**.
```json
{
"mcp": {
"tool_manager_config": {
"code_mode_binding_level": "server"
}
}
}
```
Options: `"server"` (default) or `"tool"`
```go
mcpConfig := &schemas.MCPConfig{
ToolManagerConfig: &schemas.MCPToolManagerConfig{
CodeModeBindingLevel: schemas.CodeModeBindingLevelTool, // or CodeModeBindingLevelServer
},
ClientConfigs: []schemas.MCPClientConfig{
// ... clients
},
}
```
---
## Auto-Execution with Code Mode
Code Mode tools can be auto-executed in [Agent Mode](./agent-mode), but with **additional validation**:
1. The `listToolFiles` and `readToolFile` tools are always auto-executable (they're read-only)
2. The `executeToolCode` tool is auto-executable **only if** all tool calls within the code are allowed
### How Validation Works
Validation happens at two points, and only the second one is the actual security guarantee:
1. **Pre-flight (heuristic, decides whether to ask before running).** When `executeToolCode` is
called in agent mode, Bifrost scans the generated code's source text for `serverName.toolName()`
calls and checks each against `tools_to_auto_execute` for that server. If every call it can
identify is allowed, the code runs immediately without prompting; otherwise it's returned to
the user for approval first. This is a best-effort scan over source text, not a full parse - it
can only recognize the literal `server.tool(...)` call shape.
2. **At each tool call, while the code is running (authoritative).** Every time the running code
actually invokes a tool - however it was referenced, including indirection a text scan can't
see, like `getattr(server, name)(...)` or a dispatch table - Bifrost checks that specific tool
again before making the call. When the code is running unattended in the agent loop, a tool
not in `tools_to_auto_execute` is refused at that moment, regardless of how it was called or
whether the pre-flight scan let the code run without asking. This is what makes the guarantee
below hold even for code the pre-flight step couldn't fully analyze. If a plugin pre-hook
rewrites the tool name, the rewritten name is the one checked.
Code that was returned for approval and then executed by your application (for example through
`/v1/mcp/tool/execute`) is not held to `tools_to_auto_execute`: approving it is the human
decision that list asks for. Every nested call is still limited to `tools_to_execute`.
**Example:**
```json
{
"name": "youtube",
"tools_to_execute": ["*"],
"tools_to_auto_execute": ["search"],
"is_code_mode_client": true
}
```
```python
# This code WILL auto-execute (only uses search)
results = youtube.search(query="AI")
result = results
# This code will NOT auto-execute (uses delete_video which is not in auto-execute list)
youtube.delete_video(id="abc123")
```
---
## Code Execution Environment
### Available APIs
| Available | Not Available |
|-----------|---------------|
| Python-like syntax | `import` statements |
| Synchronous tool calls | Classes (use dicts) |
| `print()` for logging | File I/O |
| Dict/List operations | Network access |
| List comprehensions | `random`, `time` modules |
| `json` (`json.decode`, `json.encode`) and `math` modules, predeclared | |
A code mode server named `json` or `math` keeps its name in the sandbox, and the module of that name is then unavailable to scripts.
### Tool Results Inside a Script
A nested tool call hands the script a value, not text:
| The server returns | The script receives |
|--------------------|---------------------|
| `structuredContent` | That object, as a dict. The text blocks beside it are ignored |
| One text block holding JSON | The decoded dict, list or scalar |
| Several text blocks, each holding JSON | A list with one element per block |
| JSON wrapped in a JSON string (`"{\"a\": 1}"`) | The decoded object or array. A wrapped scalar such as `"123"` stays a str |
| Any other text | A str, as sent. `json.decode` parses a str that holds JSON and raises on plain text |
| `isError: true` | An error value: `hasattr(r, "error")` is True and `r.error` holds the message, without a leading `Error: `. Returned as-is it becomes `{"error": ""}`. The script keeps running and decides what the failure means |
| Text starting with `Error: `, without `isError` | Nothing: the call raises, the script stops, and the error message is returned to the model |
Responses stay inside the sandbox: the model sees only the script's print output and its `result`. A nested call that answered with `isError` is the exception: the reply notes it as `[TOOL] server.tool returned an error: `, and a run that finishes ends with `Execution completed, but N nested tool calls failed.` rather than claiming a clean run.
### Handling Failed Calls
A run that finishes counts as a success, even when a nested call failed along the way. The script decides whether a failure matters: it can recover and carry on, or call `fail()` to stop the run, which marks the `executeToolCode` result as an error.
```python
# A failure the script can recover from: fall back, and the run still succeeds.
r = crm.get_contact(id=7)
if hasattr(r, "error"):
r = crm.search_contacts(query="id:7")
result = r["contacts"][0]["email"]
```
```python
# A failure that makes the whole run fail.
r = crm.update_contact(id=7, email="new@acme.com")
if hasattr(r, "error"):
fail("update failed: " + r.error)
result = "updated"
```
A call that cannot complete at all (a dropped connection or a timeout) always stops the script.
### Runtime Environment Details
**Engine:** Starlark interpreter (Python subset)
**Tool Exposure:** Tools from code mode clients are exposed as global objects:
```python
# If you have a 'youtube' code mode client with a 'search' tool
results = youtube.search(query="AI news")
```
**Code Processing:**
1. Code is validated for syntax errors
2. Tool calls are extracted and validated
3. Code executes in-process with a fresh, resource-budgeted Starlark thread
4. Result variable is automatically serialized to JSON
**Execution Limits:**
Every execution gets its own budget. The limits below are the defaults; each one is configurable (see [Configuring Execution Limits](#configuring-execution-limits)). There is no limit on how many executions run concurrently.
| Limit | Field | Default |
|-------|-------|---------|
| Submitted source code size | `max_source_bytes` | 64 KiB (65536) |
| Computation steps, including work performed inside builtins | `max_steps` | 1,000,000 |
| Estimated allocation budget, enforced by the interpreter and gateway value converters before allocating | `max_memory_bytes` | 64 MiB (67108864) |
| Print output (plus a one-line note per failed tool call), including newlines | `max_log_bytes` | 64 KiB (65536) |
| Nested tool calls | `max_tool_calls` | 64 |
| Size budget for each tool payload and converted result. Tool result strings count at their raw size; values leaving the interpreter (the `result` and tool arguments) count conservatively for JSON escaping, containers, and indentation | `max_value_bytes` | 1 MiB (1048576) |
| Nesting depth of values crossing the tool boundary | `max_nesting_depth` | 64 |
- Default timeout: 30 seconds per tool execution (`tool_execution_timeout`)
- Arguments and results must be JSON-compatible values (tuples are converted to arrays) with scalar dictionary keys. Cycles and integers exceeding 4096 bits are always rejected
- Starlark has no direct file system or network APIs. Nested tools run in the gateway with the original request's authorization and tool filters
Execution stays inside the gateway: there is no child-process startup or IPC. The runtime uses Canonical's Starlark implementation with computation, allocation, and cancellation checks. These are cooperative limits on accounted work and allocations, not an OS-enforced limit on gateway RSS. Parsing is bounded by source size; MCP transports and tools retain responsibility for their own network buffers and external work.
Exceeding a limit returns a tool error. Later executions start with a fresh scope and budget. Split large workloads into smaller executions. New host builtins must honor the same budgets and cancellation requirements.
### Configuring Execution Limits
Limits are a **global setting** applied to every code mode execution, and they reload without a restart. Set only the fields you want to change: an omitted field, or `0`, uses its default. Negative values are rejected, `max_value_bytes` must be `0` or at least 1024, and `max_nesting_depth` can be at most 1000, because value conversion recurses once per level.
Raising `max_memory_bytes` raises the budget of every concurrent execution. Size it against the gateway's memory and the number of code mode requests you expect to run at once.
Open **MCP Settings** in the workspace configuration and edit the **Code Mode Limits** fields. Leave a field empty to use its default, then save.
`PUT /api/config` takes the whole `client_config`, so read the current one, set `mcp_code_mode_limits`, and send it back:
```bash
curl -s http://localhost:8080/api/config \
| jq '{client_config: (.client_config + {mcp_code_mode_limits: {max_steps: 5000000, max_memory_bytes: 268435456, max_tool_calls: 200}})}' \
| curl -X PUT http://localhost:8080/api/config -H "Content-Type: application/json" -d @-
```
A `client_config` without `mcp_code_mode_limits` keeps the current limits; `"mcp_code_mode_limits": {}` resets every limit to its default.
```json
{
"mcp": {
"tool_manager_config": {
"code_mode_limits": {
"max_steps": 5000000,
"max_memory_bytes": 268435456,
"max_tool_calls": 200
}
}
}
}
```
`client_config.mcp_code_mode_limits` is also accepted; `mcp.tool_manager_config.code_mode_limits` takes precedence when both are set.
```yaml
bifrost:
mcp:
toolManagerConfig:
codeModeLimits:
maxSteps: 5000000
maxMemoryBytes: 268435456
maxToolCalls: 200
```
```go
mcpConfig := &schemas.MCPConfig{
ToolManagerConfig: &schemas.MCPToolManagerConfig{
CodeModeLimits: &schemas.MCPCodeModeLimits{
MaxSteps: 5_000_000,
MaxMemoryBytes: 256 << 20,
MaxToolCalls: 200,
},
},
}
// Change the limits at runtime; nil restores the defaults.
err := client.UpdateCodeModeLimits(&schemas.MCPCodeModeLimits{MaxToolCalls: 500})
```
### Error Handling
Bifrost provides detailed error messages with hints:
```
Execution runtime error:
code.star:1:10: undefined: youtube
Hints:
Variable 'youtube' is not defined.
Note: Each executeToolCode call runs in a fresh scope — no variables persist between calls.
Available server keys: filesystem, youtubeAPI
Access tools using: server_name.tool_name(param="value")
```
A runtime error also names the script lines that led to it, in the Python traceback layout, one frame per line. Deep recursion keeps only the outermost frame and the innermost five frames, and a `... N more frames ...` line stands in for the N frames left out between them:
```
Execution runtime error:
Traceback (most recent call last):
code.star:4:14: in
code.star:3:13: in main
Error: key "email" not in dict
```
### Timeouts
- Default: 30 seconds per tool call
- Configure via `tool_execution_timeout` in `tool_manager_config`
- Long-running operations are interrupted with timeout error
---
## Why Savings Grow with Tool Count
Classic MCP injects every available tool definition on every model turn. As you connect more servers, the repeated tool catalog dominates the input context, so cost rises with the size of your MCP footprint.
Code Mode keeps that catalog behind four meta-tools. The model discovers the relevant stub files, reads only the signatures and docs it needs, and executes the multi-tool workflow inside the sandbox. In the benchmark rounds above, that kept Code Mode input usage nearly flat while classic MCP grew from 19.9M to 75.1M input tokens.
The effect is most visible at large scale: with **508 tools across 16 servers**, Code Mode cut input tokens from **75.1M to 5.4M** and estimated cost from **\$377.00 to \$29.00**, while preserving a **65/65 (100%)** pass rate.
---
## Next Steps
Combine Code Mode with auto-execution
Expose your tools to external clients