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.
Enable Code Mode in my Nuxt MCP server (@nuxtjs/mcp-toolkit).
- Install secure-exec: pnpm add secure-exec
- Set experimental_codeMode: true on the handler in server/mcp/index.ts (via defineMcpHandler)
- Code Mode replaces all registered tools with a single 'code' tool
- The LLM writes JavaScript that calls tools via a codemode object (e.g. await codemode.listUsers())
- This reduces round-trips and token usage — especially with many tools (10+ tools saves 50%+ tokens)
- Code runs in a secure V8 sandbox (workerd) — no access to filesystem, network, or Node APIs
- If there are many tools (10+), consider progressive mode (experimental_codeMode: { progressive: true }) to keep both individual tools and the code tool — for fewer tools, standard mode is sufficient
- Code Mode is experimental — the API may evolve
Docs: https://mcp-toolkit.nuxt.dev/advanced/code-mode
| 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
secure-exec and Node.js >=18.16.0. The rest of the module still supports Node.js 18.x, but code mode depends on AsyncLocalStorage.snapshot() to preserve request context.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,
maxRequestBodyBytes: 1_048_576,
maxToolResponseSize: 1_048_576,
wallTimeLimitMs: 60_000,
maxToolCalls: 200,
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.1048576 (1 MB)Maximum bytes accepted in a single RPC request body from the sandbox. Returns HTTP 413 if exceeded. Prevents memory exhaustion from oversized payloads. Like memoryLimit, this applies when the RPC server first starts; call disposeCodeMode() before changing it.1048576 (1 MB)Maximum bytes for individual tool RPC responses. Large results are truncated using the same strategy as maxResultSize.60000 (60 seconds)Per-execution deadline checked at the start of each sandbox→host RPC call (tool invocation or return value). After the deadline, the next RPC receives HTTP 408. This bounds host-side work (e.g. slow tools); pure CPU loops in the isolate are primarily limited by cpuTimeLimitMs.200Maximum number of tool RPC calls per execution. Prevents runaway loops that repeatedly invoke expensive tools. Returns HTTP 429 when exceeded.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 |
| Wall-clock deadline | 60 seconds | wallTimeLimitMs | Enforced on each RPC from the sandbox — stops further tool/return RPCs after the deadline |
| Memory | 64 MB | memoryLimit | V8 isolate hard limit — prevents OOM crashes |
| Result size | 100 KB | maxResultSize | Intelligent truncation (arrays by items, objects by keys) |
| Tool response size | 1 MB | maxToolResponseSize | Per-call truncation before response delivery |
| Request body size | 1 MB | maxRequestBodyBytes | HTTP 413 early rejection — prevents memory exhaustion |
| Tool calls per execution | 200 | maxToolCalls | HTTP 429 rate limit — prevents runaway loops |
| 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.
Error Sanitization
Infrastructure errors (file paths, stack traces) are sanitized before being returned to the sandbox or the MCP client. Full error details are logged server-side with the [nuxt-mcp-toolkit] prefix for debugging.
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