---
title: "Code Mode"
description: "Let LLMs orchestrate multiple tool calls in a single JavaScript execution."
canonical_url: "https://mcp-toolkit.nuxt.dev/advanced/code-mode"
---
# Code Mode

> Let LLMs orchestrate multiple tool calls in a single JavaScript execution.

## 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.

<prompt :actions="["copy","cursor","windsurf"]" description="Enable Code Mode in the MCP server" icon="i-lucide-code">

Enable Code Mode in my Nuxt MCP server (@nuxtjs/mcp-toolkit).

- Install secure-exec: `pnpm add secure-exec` or `npm install secure-exec` or `yarn add secure-exec` or `bun add secure-exec` (Node >=18.16.0)
- 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](https://mcp-toolkit.nuxt.dev/advanced/code-mode)

</prompt>

<table>
<thead>
  <tr>
    <th>
      
    </th>
    
    <th>
      Traditional MCP
    </th>
    
    <th>
      Code Mode
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Pattern
      </strong>
    </td>
    
    <td>
      LLM calls tools one by one
    </td>
    
    <td>
      LLM writes JS that calls tools
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Round-trips
      </strong>
    </td>
    
    <td>
      One per tool call
    </td>
    
    <td>
      One for all operations
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Complex logic
      </strong>
    </td>
    
    <td>
      Multiple turns for conditionals/loops
    </td>
    
    <td>
      Native JS control flow
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Token usage
      </strong>
    </td>
    
    <td>
      Higher (repeated context)
    </td>
    
    <td>
      Lower (single invocation)
    </td>
  </tr>
</tbody>
</table>

<callout color="warning" icon="i-lucide-flask-conical">

Code Mode is **experimental for now**. The API may evolve as we stabilize this feature based on feedback.

</callout>

## 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.

<table>
<thead>
  <tr>
    <th>
      Server size
    </th>
    
    <th>
      Traditional MCP
    </th>
    
    <th>
      Code Mode
    </th>
    
    <th>
      Savings
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        10 tools
      </strong>
      
      , 3-step task
    </td>
    
    <td>
      ~1,860 tokens in tool descriptions
    </td>
    
    <td>
      ~920 tokens
    </td>
    
    <td>
      <strong>
        -51%
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        25 tools
      </strong>
      
      , 4-step task
    </td>
    
    <td>
      ~6,200 tokens
    </td>
    
    <td>
      ~1,700 tokens
    </td>
    
    <td>
      <strong>
        -73%
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        50 tools
      </strong>
      
      , 5-step task
    </td>
    
    <td>
      ~15,500 tokens
    </td>
    
    <td>
      ~3,000 tokens
    </td>
    
    <td>
      <strong>
        -81%
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        100 tools
      </strong>
      
      , 5-step task
    </td>
    
    <td>
      ~31,000 tokens
    </td>
    
    <td>
      ~5,600 tokens
    </td>
    
    <td>
      <strong>
        -82%
      </strong>
    </td>
  </tr>
</tbody>
</table>

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**.

<callout color="primary" icon="i-lucide-zap">

On a real benchmark with Claude Sonnet (9 tools, 7 tasks), Code Mode saved **15% of total tokens overall** and up to **33% on complex batch operations** — collapsing 8 tool calls into a single JavaScript block.

</callout>

### 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/else` branching without an extra LLM step
- **Loops** — `for` over data instead of repeating tool calls one by one
- **Error handling** — `try/catch` to handle failures mid-workflow

## Setup

### 1. Install secure-exec

Code Mode uses [secure-exec](https://github.com/nicolo-ribaudo/secure-exec) to run LLM-generated code in a secure V8 isolate:

<code-group>

```bash [pnpm]
pnpm add secure-exec
```

```bash [npm]
npm install secure-exec
```

```bash [yarn]
yarn add secure-exec
```

```bash [bun]
bun add secure-exec
```

</code-group>

<callout color="info" icon="i-lucide-info">

Code Mode requires `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.

</callout>

### 2. Enable on a handler

Add `experimental_codeMode` to any handler:

<code-group>

```typescript [Default handler]
// server/mcp/index.ts
export default defineMcpHandler({
  experimental_codeMode: true,
})
```

```typescript [Custom handler]
// server/mcp/ai-agent.ts
export default defineMcpHandler({
  name: 'ai-agent',
  experimental_codeMode: true,
  tools: [getUserTool, listTodosTool, createTodoTool],
})
```

</code-group>

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:

1. All registered tools are converted to TypeScript type definitions
2. A `code` tool is created with those types embedded in its description
3. The LLM writes JavaScript using a `codemode` object to call tools
4. 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:

<code-group>

```javascript [Sequential calls]
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 };
```

```javascript [Parallel calls]
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,
};
```

```javascript [Loop + conditional]
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 };
```

</code-group>

## Configuration Options

Pass an options object instead of `true` for fine-grained control:

```typescript [server/mcp/index.ts]
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,
  },
})
```

<field-group>
<field name="memoryLimit" type="number">

Default: `64`

V8 isolate memory limit in MB. Set once at first execution — call `disposeCodeMode()` to change.

</field>

<field name="cpuTimeLimitMs" type="number">

Default: `10000`

CPU time limit per execution in milliseconds. The sandbox is killed after this duration.

</field>

<field name="maxResultSize" type="number">

Default: `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.

</field>

<field name="maxRequestBodyBytes" type="number">

Default: `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.

</field>

<field name="maxToolResponseSize" type="number">

Default: `1048576` (1 MB)

Maximum bytes for individual tool RPC responses. Large results are truncated using the same strategy as `maxResultSize`.

</field>

<field name="wallTimeLimitMs" type="number">

Default: `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`.

</field>

<field name="maxToolCalls" type="number">

Default: `200`

Maximum number of tool RPC calls per execution. Prevents runaway loops that repeatedly invoke expensive tools. Returns HTTP 429 when exceeded.

</field>

<field name="progressive" type="boolean">

Default: `false`

Enable progressive disclosure mode. See [Progressive Mode](#progressive-mode) below.

</field>

<field name="description" type="string">

Default: built-in template

Custom description for the `code` tool. Supports `{{types}}` and `{{count}}` placeholders.

</field>
</field-group>

## 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:

1. **search** — discovers tools by keyword, returns their signatures
2. **code** — executes code using discovered tools

```typescript [server/mcp/index.ts]
export default defineMcpHandler({
  experimental_codeMode: {
    progressive: true,
  },
})
```

The LLM workflow becomes:

<code-group>

```txt [Step 1 — Search]
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
```

```txt [Step 2 — Execute]
LLM calls: code({ code: "..." })

→ Executes code using the discovered tools
```

</code-group>

<callout color="info" icon="i-lucide-info">

Progressive mode adds an extra tool call but significantly reduces the initial prompt size. Only use it when you have a large number of tools — for small toolsets, standard mode is more efficient.

</callout>

## Custom Description

Override the `code` tool description to customize LLM instructions:

```typescript [server/mcp/index.ts]
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](https://v8.dev/docs/embed) 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.1` and `localhost` are 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-token` header.
- **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

<table>
<thead>
  <tr>
    <th>
      Resource
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Configurable
    </th>
    
    <th>
      Protection
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        CPU time
      </strong>
    </td>
    
    <td>
      10 seconds
    </td>
    
    <td>
      <code>
        cpuTimeLimitMs
      </code>
    </td>
    
    <td>
      Sandbox is killed on timeout — prevents infinite loops
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Wall-clock deadline
      </strong>
    </td>
    
    <td>
      60 seconds
    </td>
    
    <td>
      <code>
        wallTimeLimitMs
      </code>
    </td>
    
    <td>
      Enforced on each RPC from the sandbox — stops further tool/return RPCs after the deadline
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Memory
      </strong>
    </td>
    
    <td>
      64 MB
    </td>
    
    <td>
      <code>
        memoryLimit
      </code>
    </td>
    
    <td>
      V8 isolate hard limit — prevents OOM crashes
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Result size
      </strong>
    </td>
    
    <td>
      100 KB
    </td>
    
    <td>
      <code>
        maxResultSize
      </code>
    </td>
    
    <td>
      Intelligent truncation (arrays by items, objects by keys)
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Tool response size
      </strong>
    </td>
    
    <td>
      1 MB
    </td>
    
    <td>
      <code>
        maxToolResponseSize
      </code>
    </td>
    
    <td>
      Per-call truncation before response delivery
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Request body size
      </strong>
    </td>
    
    <td>
      1 MB
    </td>
    
    <td>
      <code>
        maxRequestBodyBytes
      </code>
    </td>
    
    <td>
      HTTP 413 early rejection — prevents memory exhaustion
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Tool calls per execution
      </strong>
    </td>
    
    <td>
      200
    </td>
    
    <td>
      <code>
        maxToolCalls
      </code>
    </td>
    
    <td>
      HTTP 429 rate limit — prevents runaway loops
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Log entries
      </strong>
    </td>
    
    <td>
      200
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Console output capped — prevents <code>
        console.log
      </code>
      
       flooding
    </td>
  </tr>
</tbody>
</table>

### 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

<callout color="info" icon="i-lucide-shield">

The sandbox can **only** communicate with your registered MCP tools via the authenticated RPC bridge. It cannot access the filesystem, network, environment, or any other host resources.

</callout>

## 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.

<code-group>

```typescript [With Middleware]
// server/mcp/index.ts
export default defineMcpHandler({
  experimental_codeMode: true,
  middleware: async (event) => {
    const user = await getUser(event).catch(() => null)
    if (user) event.context.user = user
    // Soft auth — see /advanced/middleware. Use `enabled` guards to
    // hide tools from anonymous callers instead of throwing 401 here.
  },
})
```

```typescript [With Dynamic Definitions]
// 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
  },
})
```

```typescript [With Progressive + Auth]
// 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) return
    const user = await getUserByApiKey(apiKey).catch(() => null)
    if (user) event.context.user = user
  },
})
```

</code-group>

[Middleware](https://mcp-toolkit.nuxt.dev/advanced/middleware) runs before tool execution — your tools access `event.context` as usual. Tools with [`enabled` guards](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) 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:

<table>
<thead>
  <tr>
    <th>
      MCP Name
    </th>
    
    <th>
      JavaScript Name
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        get-user
      </code>
    </td>
    
    <td>
      <code>
        get_user
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        list-todos
      </code>
    </td>
    
    <td>
      <code>
        list_todos
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        123-tool
      </code>
    </td>
    
    <td>
      <code>
        _123_tool
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        delete
      </code>
    </td>
    
    <td>
      <code>
        delete_
      </code>
    </td>
  </tr>
</tbody>
</table>

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):

```typescript
import { disposeCodeMode } from '#imports'

// In a shutdown hook or cleanup function
disposeCodeMode()
```

## Next Steps

- [Handlers](https://mcp-toolkit.nuxt.dev/handlers/overview) - Create custom MCP endpoints
- [Middleware](https://mcp-toolkit.nuxt.dev/advanced/middleware) - Add authentication
- [Dynamic Definitions](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) - Conditionally register tools
- [Evals](https://mcp-toolkit.nuxt.dev/advanced/evals) - Benchmark code mode vs standard MCP


## Sitemap

See the full [sitemap](https://mcp-toolkit.nuxt.dev/sitemap.md) for all pages.
