Code Mode
What is Code Mode?
Code Mode replaces the traditional multi-turn tool calling pattern with a single code tool. Instead of the LLM calling tools one at a time — each requiring a round-trip — it writes JavaScript code that orchestrates multiple tools in one execution.
| Traditional MCP | Code Mode | |
|---|---|---|
| Pattern | LLM calls tools one by one | LLM writes JS that calls tools |
| Round-trips | One per tool call | One for all operations |
| Complex logic | Multiple turns for conditionals/loops | Native JS control flow |
| Token usage | Higher (repeated context) | Lower (single invocation) |
Why Code Mode?
Every LLM round-trip resends all tool descriptions as context. With traditional MCP, a task that requires 5 steps with 50 tools sends the full tool catalog 5 times — that's 15,500 tokens just for tool descriptions. Code Mode sends compact TypeScript signatures in a single tool, cutting that to ~3,000 tokens.
The scaling problem
In traditional MCP, tool description overhead scales as tools × round-trips. Code Mode replaces all tools with one code tool containing compact type signatures — and usually needs fewer round-trips.
| Server size | Traditional MCP | Code Mode | Savings |
|---|---|---|---|
| 10 tools, 3-step task | ~1,860 tokens in tool descriptions | ~920 tokens | -51% |
| 25 tools, 4-step task | ~6,200 tokens | ~1,700 tokens | -73% |
| 50 tools, 5-step task | ~15,500 tokens | ~3,000 tokens | -81% |
| 100 tools, 5-step task | ~31,000 tokens | ~5,600 tokens | -82% |
These numbers represent tool description overhead only. Total savings depend on the task, but the trend is clear: the more tools you have, the bigger the savings.
Beyond token savings
Code Mode also unlocks patterns that traditional MCP cannot do efficiently:
- Parallel execution —
Promise.all()for independent calls instead of sequential round-trips - Conditional logic —
if/elsebranching without an extra LLM step - Loops —
forover data instead of repeating tool calls one by one - Error handling —
try/catchto handle failures mid-workflow
Setup
1. Install secure-exec
Code Mode uses secure-exec to run LLM-generated code in a secure V8 isolate:
pnpm add secure-exec
npm install secure-exec
yarn add secure-exec
bun add secure-exec
2. Enable on a handler
Add experimental_codeMode to any handler:
// server/mcp/index.ts
export default defineMcpHandler({
experimental_codeMode: true,
})
// server/mcp/ai-agent.ts
export default defineMcpHandler({
name: 'ai-agent',
experimental_codeMode: true,
tools: [getUserTool, listTodosTool, createTodoTool],
})
That's it. The module replaces all your tools with a single code tool that the LLM uses to orchestrate them.
How It Works
When Code Mode is enabled:
- All registered tools are converted to TypeScript type definitions
- A
codetool is created with those types embedded in its description - The LLM writes JavaScript using a
codemodeobject to call tools - The code runs in a V8 isolate with only RPC access to your tools
Example: what the LLM generates
Given tools get-user, list-todos, and create-todo, the LLM receives type definitions and writes code like:
const user = await codemode.get_user({ id: "123" });
const todos = await codemode.list_todos({ userId: user.id });
if (todos.length === 0) {
await codemode.create_todo({
title: "Welcome task",
userId: user.id,
});
}
return { user, todos };
const [users, products, orders] = await Promise.all([
codemode.list_users(),
codemode.list_products(),
codemode.list_orders({ status: "pending" }),
]);
return {
userCount: users.length,
productCount: products.length,
pendingOrders: orders.length,
};
const users = await codemode.list_users();
const results = [];
for (const user of users) {
const todos = await codemode.list_todos({ userId: user.id });
if (todos.some(t => t.overdue)) {
await codemode.send_reminder({ userId: user.id });
results.push(user.name);
}
}
return { reminded: results };
Configuration Options
Pass an options object instead of true for fine-grained control:
export default defineMcpHandler({
experimental_codeMode: {
memoryLimit: 64,
cpuTimeLimitMs: 10_000,
maxResultSize: 102_400,
progressive: false,
description: undefined,
},
})
64V8 isolate memory limit in MB. Set once at first execution — call disposeCodeMode() to change.10000CPU time limit per execution in milliseconds. The sandbox is killed after this duration.102400 (100 KB)Maximum result size in bytes before truncation. Large results are intelligently truncated — arrays by number of items, objects by number of keys.falseEnable progressive disclosure mode. See Progressive Mode below.code tool. Supports {{types}} and {{count}} placeholders.Progressive Mode
When your server exposes many tools (50+), embedding all type definitions in the code tool description becomes expensive in tokens. Progressive mode solves this by splitting into two tools:
search— discovers tools by keyword, returns their signaturescode— executes code using discovered tools
export default defineMcpHandler({
experimental_codeMode: {
progressive: true,
},
})
The LLM workflow becomes:
LLM calls: search({ query: "user" })
→ Found 2/12 tools matching "user":
codemode.get_user: (input: { id: string }) => Promise<unknown>; // Get user by ID
codemode.list_users: () => Promise<unknown>; // List all users
LLM calls: code({ code: "..." })
→ Executes code using the discovered tools
Custom Description
Override the code tool description to customize LLM instructions:
export default defineMcpHandler({
experimental_codeMode: {
description: `You have {{count}} tools available. Write JavaScript using the codemode object.
{{types}}
Always combine related operations into a single code block.`,
},
})
The {{types}} placeholder is replaced with the generated TypeScript definitions. The {{count}} placeholder is replaced with the number of available tools.
In progressive mode, {{types}} is not available since types are discovered via the search tool.
Security
Running LLM-generated code requires serious security measures. Code Mode implements defense in depth across 7 layers to ensure the sandbox cannot escape, access unauthorized resources, or exhaust host resources.
Sandbox Isolation
LLM-generated code runs in a separate V8 isolate via secure-exec. This is the same isolation technology used by Cloudflare Workers and similar platforms. The sandbox has:
- No filesystem access — cannot read, write, or list files
- No Node.js APIs — no
require(),import(),process,fs,child_process, etc. - No environment variables — cannot read secrets or configuration
- No host process access — cannot modify the parent process in any way
Network Restrictions
The sandbox can only communicate with the internal RPC server. All other network access is blocked:
- Port-locked — Only the randomly-assigned RPC port is accessible. Other localhost services (databases, admin panels, other apps) are blocked.
- Host-locked — Only
127.0.0.1andlocalhostare allowed. External hosts are rejected. - No DNS — DNS resolution is disabled entirely.
- No redirects — HTTP redirects are rejected (
redirect: 'error'), preventing SSRF via open redirects.
RPC Authentication
Communication between the sandbox and the host uses a per-session cryptographic token:
- 256-bit token — Generated with
crypto.randomBytes(32)at RPC server startup. - Header-based auth — Every request must include the token via
x-rpc-tokenheader. - 403 on mismatch — Requests without a valid token are rejected immediately.
This prevents other local processes from calling your MCP tools through the RPC port.
Resource Limits
| Resource | Default | Configurable | Protection |
|---|---|---|---|
| CPU time | 10 seconds | cpuTimeLimitMs | Sandbox is killed on timeout — prevents infinite loops |
| Memory | 64 MB | memoryLimit | V8 isolate hard limit — prevents OOM crashes |
| Result size | 100 KB | maxResultSize | Intelligent truncation (arrays by items, objects by keys) |
| Log entries | 200 | No | Console output capped — prevents console.log flooding |
Input Validation
Tool names are interpolated into the sandbox code template. To prevent code injection:
- Strict identifier regex — Every tool name is validated against
/^[\w$]+$/before being injected into the sandbox template. - Sanitization — Names are sanitized upstream (
get-user→get_user), but a second validation layer at the template level ensures defense in depth. - Rejection — If a name fails validation, the execution throws immediately — no partial injection.
Summary
Usage with Other Features
Code Mode is fully compatible with other module features. Your tools remain unchanged — only the way they are exposed to the LLM changes.
// server/mcp/index.ts
export default defineMcpHandler({
experimental_codeMode: true,
middleware: async (event) => {
const user = await getUser(event)
if (!user) {
throw createError({ statusCode: 401 })
}
event.context.user = user
},
})
// server/mcp/tools/admin-tool.ts
export default defineMcpTool({
name: 'admin-delete',
description: 'Delete a resource (admin only)',
enabled: event => event.context.user?.role === 'admin',
inputSchema: {
id: z.string(),
},
handler: async ({ id }) => {
// Only visible in code mode when user is admin
},
})
// server/mcp/index.ts
export default defineMcpHandler({
experimental_codeMode: {
progressive: true,
cpuTimeLimitMs: 15_000,
},
middleware: async (event) => {
const apiKey = getHeader(event, 'x-api-key')
if (!apiKey) throw createError({ statusCode: 401 })
event.context.user = await getUserByApiKey(apiKey)
},
})
Middleware runs before tool execution — your tools access event.context as usual. Tools with enabled guards are excluded from the generated type definitions and the codemode object.
Tool Name Sanitization
MCP tool names (kebab-case) are automatically converted to valid JavaScript identifiers for the codemode object:
| MCP Name | JavaScript Name |
|---|---|
get-user | get_user |
list-todos | list_todos |
123-tool | _123_tool |
delete | delete_ |
Reserved JavaScript words are suffixed with _. Names starting with a digit are prefixed with _.
Cleanup
Call disposeCodeMode() during shutdown to release resources (V8 runtime, RPC server):
import { disposeCodeMode } from '#imports'
// In a shutdown hook or cleanup function
disposeCodeMode()
Next Steps
- Handlers - Create custom MCP endpoints
- Middleware - Add authentication
- Dynamic Definitions - Conditionally register tools
- Evals - Benchmark code mode vs standard MCP