---
title: "Code mode"
url: "/docs/mcp-gateway/code-mode/"
kind: "feature"
summary: "Serve one execute tool: the agent sends TypeScript that finds, calls and combines tools, and Arion runs it in a sandbox and returns one result."
---

> Arion site index: [/llms.txt](/llms.txt)

# Code mode

Serve one execute tool: the agent sends TypeScript that finds, calls and combines tools, and Arion runs it in a sandbox and returns one result.

{{ <badge label="requires: codemode" kind="requires" /> }}

Code mode replaces a gateway's tool list with a single `execute` tool: the agent sends a short TypeScript script that discovers, calls and combines your tools, and Arion runs it in a sandbox and returns only the final value. Use it for multi-step work and large catalogs, where calling tools one model turn at a time costs round trips and fills the context window with intermediate results.

## Why it matters

In [tools mode](@/docs/mcp-gateway/overview.md#tools-mode-and-code-mode) the model is the orchestrator. It calls one tool, waits, reads the whole result, then decides the next call, so a five-step task takes five turns. Every intermediate payload also lands in the model's context, even when it is needed only for a moment. Code mode lets the model do what it does best — write code: the same steps become one small program that runs next to the tools. The tool calls still happen, but on the gateway's side of the boundary and in parallel where the script allows it. Only the answer crosses back.

{{ <figure src="img/docs/code-mode.svg" alt="In tools mode the agent and Arion exchange one request and one response per tool call; in code mode the agent sends one script, Arion runs it in a sandbox that calls the tools, and one result comes back." caption="The tool calls do not disappear: they move to the gateway, where they are fast and stay out of the model's context." label="Figure 1." /> }}

Code mode changes how tools are called, never what is allowed. Every call a script makes goes through the same tool registry as a normal `tools/call`: the same [per-tool authorization](@/docs/mcp-gateway/tool-authorization.md), schema validation, timeouts, size limits and metrics.

## Before you begin

- Build Arion with the `codemode` Cargo feature: `cargo build --release -p arion-proxy --features codemode`. A binary built without it rejects a `code_mode` configuration at startup rather than ignoring it.
- Tools are called by qualified name, `namespace.name`. Every static tool has a `namespace`, and [dynamic servers](@/docs/mcp-gateway/federation.md) and [OpenAPI sources](@/docs/mcp-gateway/openapi-tools.md) supply one for the tools they add.
- Prefer tools with an `output_schema`. Their results reach the script as parsed JSON; without one, the script receives the response body as a string.

## Enable it

A gateway runs in exactly one mode. Replace `tools_mode: {}` with `code_mode`; every limit has a default, so an empty block is enough:

```yaml,arion=fragment
typed_config:
  "@type": type.googleapis.com/arion.extensions.filters.http.mcp.mcp_gateway.v3.McpGateway
  server_info:
    name: support-tools
    version: "1.0.0"
  code_mode: {}
  tools: []   # tools, dynamic_mcp_servers, open_api_tool_sources and tds work as in tools mode
```

In the [Builder](@/builder.md), type `code mode` to add it to an MCP gateway config, with `timeout` and `calls` for its limits.

The registry is built exactly as in tools mode. What changes is the surface agents see:

- `initialize` returns `instructions` that point the client at `execute`.
- `tools/list` returns one tool, `execute`, whose arguments are `code` (the script) and an optional `timeout_ms`. `tool_search` and `POST /mcp/search` are not served; discovery happens inside the script.
- The `execute` description is written for each caller. It lists the toolkits and namespaces that caller may use and the catalog calls for finding more. It is capped at 4 KiB, so a catalog of thousands of tools still gives the model a compact starting point.

## Write a script

The script is the body of an async function: `await` works at the top level, and `return` gives back the value. TypeScript types are stripped before the script runs.

```typescript
// Look up a customer, fetch history and plan in parallel, then open a ticket.
const customer = await tools["crm.find_customer"]({ email: "ada@example.com" });
if (!customer.ok) return { error: customer.error.code };

const [history, plan] = await Promise.all([
  tools["support.recent_tickets"]({ customer_id: customer.data.id }),
  tools["billing.current_plan"]({ customer_id: customer.data.id }),
]);

const ticket = await tools["support.create_ticket"]({
  ticket: {
    customer_id: customer.data.id,
    priority: plan.ok && plan.data.tier === "enterprise" ? "high" : "normal",
    summary: "Refund not received",
  },
});
console.log("recent tickets:", history.ok ? history.data.items.length : "unavailable");
return { ticket: ticket.ok ? ticket.data.id : null };
```

### The script SDK

A script can use three globals.

**`tools`** calls a tool: `tools["namespace.name"](args)`, or `tools.crm.find_customer(args)`. Every call resolves to the same envelope — `{ ok: true, data }` or `{ ok: false, error: { code, message, retryable } }` — so a failed call is a value to branch on, not an exception. When the tool declares an `output_schema`, `data` is parsed JSON, not text to re-parse. Calls run concurrently under `Promise.all`, up to the gateway's concurrency limit.

**`catalog`** is discovery inside the script — the counterpart of `tool_search` in [tools mode](@/docs/mcp-gateway/tools-mode.md), without the extra round trip. It has six operations, and every one of them sees only the tools the caller is authorized for:

- `catalog.namespaces.list()` — the caller's namespaces.
- `catalog.tools.list({ namespace })` — page through one namespace.
- `catalog.tools.search({ query, namespace?, limit? })` — the same BM25 or embedding ranking as tool search, over every permitted tool, returning compact entries.
- `catalog.tools.describe({ namespace, name })` — one tool's full description and schemas.
- `catalog.toolkits.list()` and `catalog.toolkits.get({ name })` — the toolkit calls below.

A typical script needs little of it: the `execute` description already names the caller's toolkits and namespaces, so the usual sequence is one `toolkits.get` — or one `search` and a `describe` — and then straight to `tools` calls.

**`console.log(...)`** records a line that comes back with the result.

The script's return value comes back as one structured result, together with the logs and a record of every tool call with its timing and outcome. The full API, the call envelopes, every error code, the result format and the sandbox limits are in the [code mode SDK reference](@/reference/sdk/code-mode-sdk-and-limits.md).

## Toolkits

A toolkit is a named set of related tools with instructions on how to compose them, authored in configuration. Where the raw catalog tells the model what exists, a toolkit tells it what to do: find the customer, then open a ticket, at which priority.

Toolkits are also the fastest form of discovery. A toolkit's name and description appear in the `execute` description, so the model starts every session already knowing it exists. One `catalog.toolkits.get({ name })` call then returns the instructions together with every member tool's name, invocation and schemas — replacing the `namespaces.list`, `search` and per-tool `describe` calls the script would otherwise spend before doing any work:

```yaml,arion=fragment
code_mode:
  toolkits:
    - name: resolve_support_request
      description: Find a customer, check their plan and open a ticket at the right priority.
      instructions: >-
        Find the customer by email first. Fetch recent tickets and the current plan
        in parallel, then create one ticket; use priority high for enterprise plans.
      tools:
        - { namespace: crm, name: find_customer }
        - { namespace: support, name: recent_tickets }
        - { namespace: billing, name: current_plan }
        - { namespace: support, name: create_ticket }
```

A toolkit needs at least two tools. A caller sees it only when every tool in it exists and that caller may use all of them, so a partial bundle never leaks. Toolkits can also be added, replaced and removed at runtime as TDS `Toolkit` resources; see [tool discovery over xDS](@/docs/mcp-gateway/tool-discovery-tds.md).

## Set the limits

Each listener bounds its own executions:

```yaml,arion=fragment
code_mode:
  execution_timeout: 20s         # wall clock per run (default 30s)
  max_host_calls: 50             # tool, catalog and log calls per run (default 100)
  max_concurrent_executions: 16  # runs at once on this listener (default 32)
  max_concurrent_tool_calls: 8   # calls in flight within one run (default 8)
  max_output_bytes: 262144       # size of the result (default 1 MiB)
  memory_limit_bytes: 33554432   # sandbox memory (default 64 MiB)
```

Every run also has a fixed 5-second compute budget, and time spent waiting on a tool does not count against it. Scripts can be at most 256 KiB. A process-wide ceiling under [`runtime.codemode`](@/reference/configuration/file-format.md#runtime) caps the sandbox pool and the memory per sandbox for all listeners together. A listener that asks for more than that ceiling fails to load.

## Verify

Initialize a session and export `SESSION_ID` as shown in [tools and REST backends](@/docs/mcp-gateway/tools-and-rest-backends.md#verify), then run a script:

```bash
curl -s http://127.0.0.1:8080/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H "mcp-session-id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"execute","arguments":{"code":"return await catalog.namespaces.list()"}}}'
```

A `tools/list` on the same session returns a single tool named `execute`. The call's `structuredContent` has `"status": "completed"`, and its `result` holds the script's value, here the catalog envelope `{"ok": true, "data": {"items": [{"namespace": "crm", "toolCount": 1}, …]}}`. The empty `logs` and `toolCalls` lists sit next to it.

## Limits

- One mode per gateway. A code-mode listener serves only `execute`; run a second listener in tools mode for clients that need the classic tool list.
- Scripts have no files, network or environment. The only ways out of the sandbox are a tool call, a catalog call and a log line.
- TypeScript is strip-only: annotations are removed, but `enum`, `namespace` declarations and decorators are rejected with a parse error.
- Runs beyond `max_concurrent_executions` are refused rather than queued. The client gets a JSON-RPC error and should retry.
- Each run starts from a fresh environment and nothing carries over between scripts, so keep state in your tools.

The `code_mode` and toolkit fields are in the [filter reference](@/reference/filters/http/mcp-gateway.md#code-mode). A complete setup is in the [code mode guide](@/guides/mcp/code-mode-for-agents.md).
